From 4da2fd9191d43a75c7a6c4cc848cd048e6700844 Mon Sep 17 00:00:00 2001 From: Wouter Admiraal Date: Wed, 28 Dec 2022 16:23:39 +0100 Subject: [PATCH] [NO JIRA] Migrate Code tests to RTL --- server/sonar-web/build.gradle | 2 +- .../sonar-web/src/main/js/api/components.ts | 2 +- .../src/main/js/api/mocks/CodeServiceMocks.ts | 168 --- .../js/api/mocks/ComponentsServiceMock.ts | 631 +++++++++++ .../main/js/api/mocks/IssuesServiceMock.ts | 108 +- .../js/api/mocks/SourceViewerServiceMock.ts | 286 ----- .../main/js/apps/code/__tests__/Code-it.ts | 431 +++++++- .../main/js/apps/code/components/CodeApp.tsx | 32 +- .../main/js/apps/code/components/Search.tsx | 21 +- .../code/components/SourceViewerWrapper.tsx | 46 +- .../components/__tests__/CodeApp-test.tsx | 286 ----- .../__tests__/ComponentMeasure-test.tsx | 72 -- .../__tests__/ComponentName-test.tsx | 131 --- .../components/__tests__/Components-test.tsx | 82 -- .../__tests__/ComponentsHeader-test.tsx | 47 - .../code/components/__tests__/Search-test.tsx | 123 --- .../__snapshots__/CodeApp-test.tsx.snap | 984 ------------------ .../ComponentMeasure-test.tsx.snap | 33 - .../__snapshots__/ComponentName-test.tsx.snap | 418 -------- .../__snapshots__/Components-test.tsx.snap | 308 ------ .../ComponentsHeader-test.tsx.snap | 94 -- .../__snapshots__/Search-test.tsx.snap | 100 -- .../js/apps/issues/__tests__/IssuesApp-it.tsx | 23 +- .../__tests__/SourceViewer-it.tsx | 96 +- .../components/hoc/withKeyboardNavigation.tsx | 2 +- .../src/main/js/helpers/mocks/sources.ts | 34 +- 26 files changed, 1248 insertions(+), 3312 deletions(-) delete mode 100644 server/sonar-web/src/main/js/api/mocks/CodeServiceMocks.ts create mode 100644 server/sonar-web/src/main/js/api/mocks/ComponentsServiceMock.ts delete mode 100644 server/sonar-web/src/main/js/api/mocks/SourceViewerServiceMock.ts delete mode 100644 server/sonar-web/src/main/js/apps/code/components/__tests__/CodeApp-test.tsx delete mode 100644 server/sonar-web/src/main/js/apps/code/components/__tests__/ComponentMeasure-test.tsx delete mode 100644 server/sonar-web/src/main/js/apps/code/components/__tests__/ComponentName-test.tsx delete mode 100644 server/sonar-web/src/main/js/apps/code/components/__tests__/Components-test.tsx delete mode 100644 server/sonar-web/src/main/js/apps/code/components/__tests__/ComponentsHeader-test.tsx delete mode 100644 server/sonar-web/src/main/js/apps/code/components/__tests__/Search-test.tsx delete mode 100644 server/sonar-web/src/main/js/apps/code/components/__tests__/__snapshots__/CodeApp-test.tsx.snap delete mode 100644 server/sonar-web/src/main/js/apps/code/components/__tests__/__snapshots__/ComponentMeasure-test.tsx.snap delete mode 100644 server/sonar-web/src/main/js/apps/code/components/__tests__/__snapshots__/ComponentName-test.tsx.snap delete mode 100644 server/sonar-web/src/main/js/apps/code/components/__tests__/__snapshots__/Components-test.tsx.snap delete mode 100644 server/sonar-web/src/main/js/apps/code/components/__tests__/__snapshots__/ComponentsHeader-test.tsx.snap delete mode 100644 server/sonar-web/src/main/js/apps/code/components/__tests__/__snapshots__/Search-test.tsx.snap diff --git a/server/sonar-web/build.gradle b/server/sonar-web/build.gradle index 0cafa2c1cc4..3e5012239d4 100644 --- a/server/sonar-web/build.gradle +++ b/server/sonar-web/build.gradle @@ -2,7 +2,7 @@ sonar { properties { property "sonar.projectName", "${projectTitle} :: Web" property "sonar.sources", "src/main/js" - property "sonar.exclusions", "src/main/js/**/__tests__/**,src/main/js/**/__mocks__/**,src/main/js/@types/**,src/main/js/helpers/mocks/**,src/main/js/helpers/testUtils.ts,src/main/js/helpers/testMocks.ts,src/main/js/helpers/testReactTestingUtils.tsx" + property "sonar.exclusions", "src/main/js/**/__tests__/**,src/main/js/**/__mocks__/**,src/main/js/@types/**,src/main/js/helpers/mocks/**,src/main/js/api/mocks/**,src/main/js/helpers/testUtils.ts,src/main/js/helpers/testMocks.ts,src/main/js/helpers/testReactTestingUtils.tsx" property "sonar.tests", "src/main/js" property "sonar.test.inclusions", "src/main/js/**/__tests__/**" property "sonar.coverage.exclusions", "src/main/js/api/**,src/main/js/**/routes.ts,src/main/js/app/index.ts,src/main/js/app/utils/startReactApp.tsx,src/main/js/components/icons/**" diff --git a/server/sonar-web/src/main/js/api/components.ts b/server/sonar-web/src/main/js/api/components.ts index 5ed1fc90d08..aa9c228712d 100644 --- a/server/sonar-web/src/main/js/api/components.ts +++ b/server/sonar-web/src/main/js/api/components.ts @@ -140,7 +140,7 @@ export function getComponent( return getJSON('/api/measures/component', data); } -type GetTreeParams = { +export type GetTreeParams = { asc?: boolean; component: string; p?: number; diff --git a/server/sonar-web/src/main/js/api/mocks/CodeServiceMocks.ts b/server/sonar-web/src/main/js/api/mocks/CodeServiceMocks.ts deleted file mode 100644 index 2c9a8021110..00000000000 --- a/server/sonar-web/src/main/js/api/mocks/CodeServiceMocks.ts +++ /dev/null @@ -1,168 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2022 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 { cloneDeep, flatMap, map } from 'lodash'; -import { mockComponent, mockComponentMeasure } from '../../helpers/mocks/component'; -import { ComponentQualifier } from '../../types/component'; -import { RawIssuesResponse } from '../../types/issues'; -import { ComponentMeasure, Metric, Paging } from '../../types/types'; -import { getChildren, getComponentTree } from '../components'; -import { searchIssues } from '../issues'; - -interface ComponentTree { - component: ComponentMeasure; - child: ComponentTree[]; -} - -function isLeaf(node: ComponentTree) { - return node.child.length === 0; -} - -function listChildComponent(node: ComponentTree): ComponentMeasure[] { - return map(node.child, (n) => n.component); -} - -function listAllComponent(node: ComponentTree): ComponentMeasure[] { - if (isLeaf(node)) { - return [node.component]; - } - - return [node.component, ...flatMap(node.child, listAllComponent)]; -} - -function listLeavesComponent(node: ComponentTree): ComponentMeasure[] { - if (isLeaf(node)) { - return [node.component]; - } - return flatMap(node.child, listLeavesComponent); -} - -export default class CodeServiceMock { - componentTree: ComponentTree; - - constructor() { - this.componentTree = { - component: mockComponentMeasure(), - child: [ - { - component: mockComponentMeasure(false, { - key: 'foo:folerA', - name: 'folderA', - path: 'folderA', - qualifier: ComponentQualifier.Directory, - }), - child: [ - { - component: mockComponentMeasure(true, { - key: 'foo:folderA/out.tsx', - name: 'out.tsx', - path: 'folderA/out.tsx', - }), - child: [], - }, - ], - }, - { - component: mockComponentMeasure(true, { - key: 'foo:index.tsx', - name: 'index.tsx', - path: 'index.tsx', - }), - child: [], - }, - ], - }; - (getComponentTree as jest.Mock).mockImplementation(this.handleGetComponentTree); - (getChildren as jest.Mock).mockImplementation(this.handleGetChildren); - (searchIssues as jest.Mock).mockImplementation(this.handleSearchIssue); - } - - findBaseComponent(key: string, from = this.componentTree): ComponentTree | undefined { - if (from.component.key === key) { - return from; - } - return from.child.find((node) => this.findBaseComponent(key, node)); - } - - handleGetChildren = ( - component: string - ): Promise<{ - baseComponent: ComponentMeasure; - components: ComponentMeasure[]; - metrics: Metric[]; - paging: Paging; - }> => { - return this.handleGetComponentTree('children', component); - }; - - handleGetComponentTree = ( - strategy: string, - component: string - ): Promise<{ - baseComponent: ComponentMeasure; - components: ComponentMeasure[]; - metrics: Metric[]; - paging: Paging; - }> => { - const base = this.findBaseComponent(component); - let components: ComponentMeasure[] = []; - if (base === undefined) { - return Promise.reject({ - errors: [{ msg: `No component has been found for id ${component}` }], - }); - } - if (strategy === 'all' || strategy === '') { - components = listAllComponent(base); - } else if (strategy === 'children') { - components = listChildComponent(base); - } else if (strategy === 'leaves') { - components = listLeavesComponent(base); - } - - return this.reply({ - baseComponent: base.component, - components, - metrics: [], - paging: { - pageIndex: 1, - pageSize: 100, - total: components.length, - }, - }); - }; - - handleSearchIssue = (): Promise => { - return this.reply({ - components: [], - effortTotal: 1, - facets: [], - issues: [], - languages: [], - paging: { total: 0, pageIndex: 1, pageSize: 100 }, - }); - }; - - getRootComponent() { - return mockComponent(this.componentTree.component); - } - - reply(response: T): Promise { - return Promise.resolve(cloneDeep(response)); - } -} diff --git a/server/sonar-web/src/main/js/api/mocks/ComponentsServiceMock.ts b/server/sonar-web/src/main/js/api/mocks/ComponentsServiceMock.ts new file mode 100644 index 00000000000..8308107d00f --- /dev/null +++ b/server/sonar-web/src/main/js/api/mocks/ComponentsServiceMock.ts @@ -0,0 +1,631 @@ +/* + * SonarQube + * Copyright (C) 2009-2022 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 { cloneDeep, flatMap, map, pick, times } from 'lodash'; +import { mockComponent } from '../../helpers/mocks/component'; +import { + mockDuplicatedFile, + mockDuplication, + mockDuplicationBlock, + mockSourceLine, + mockSourceViewerFile, +} from '../../helpers/mocks/sources'; +import { HttpStatus, RequestData } from '../../helpers/request'; +import { BranchParameters } from '../../types/branch-like'; +import { ComponentQualifier, TreeComponent, Visibility } from '../../types/component'; +import { + Component, + ComponentMeasure, + Dict, + DuplicatedFile, + Duplication, + Measure, + Metric, + Paging, + SourceLine, + SourceViewerFile, +} from '../../types/types'; +import { + getChildren, + getComponentData, + getComponentForSourceViewer, + getComponentTree, + getDuplications, + getSources, + getTree, + GetTreeParams, +} from '../components'; + +interface ComponentTree { + component: Component; + ancestors: Component[]; + measures?: Measure[]; + children: ComponentTree[]; +} + +interface SourceFile { + component: SourceViewerFile; + lines: SourceLine[]; + duplication?: { + duplications: Duplication[]; + files: Dict; + }; +} + +function isLeaf(node: ComponentTree) { + return node.children.length === 0; +} + +function listChildComponent(node: ComponentTree): Component[] { + return map(node.children, (n) => n.component); +} + +function listAllComponent(node: ComponentTree): Component[] { + if (isLeaf(node)) { + return [node.component]; + } + + return [node.component, ...flatMap(node.children, listAllComponent)]; +} + +function listLeavesComponent(node: ComponentTree): Component[] { + if (isLeaf(node)) { + return [node.component]; + } + return flatMap(node.children, listLeavesComponent); +} + +export default class ComponentsServiceMock { + failLoadingComponentStatus: HttpStatus | undefined = undefined; + defaultComponents: ComponentTree[]; + components: ComponentTree[]; + defaultSourceFiles: SourceFile[]; + sourceFiles: SourceFile[]; + + constructor(components?: ComponentTree[], sourceFiles?: SourceFile[]) { + const baseComponent = mockComponent({ + key: 'foo', + name: 'Foo', + breadcrumbs: [{ key: 'foo', name: 'Foo', qualifier: ComponentQualifier.Project }], + }); + const folderComponent = mockComponent({ + key: 'foo:folderA', + name: 'folderA', + path: 'folderA', + qualifier: ComponentQualifier.Directory, + breadcrumbs: [ + ...baseComponent.breadcrumbs, + { key: 'foo:folderA', name: 'folderA', qualifier: ComponentQualifier.Directory }, + ], + }); + this.defaultComponents = components || [ + { + component: baseComponent, + ancestors: [], + children: [ + { + component: folderComponent, + ancestors: [baseComponent], + children: [ + { + component: mockComponent({ + key: 'foo:folderA/out.tsx', + name: 'out.tsx', + path: 'folderA/out.tsx', + qualifier: ComponentQualifier.File, + breadcrumbs: [ + ...folderComponent.breadcrumbs, + { + key: 'foo:folderA/out.tsx', + name: 'out.tsx', + qualifier: ComponentQualifier.File, + }, + ], + }), + ancestors: [baseComponent, folderComponent], + children: [], + }, + ], + }, + { + component: mockComponent({ + key: 'foo:index.tsx', + name: 'index.tsx', + path: 'index.tsx', + qualifier: ComponentQualifier.File, + breadcrumbs: [ + ...baseComponent.breadcrumbs, + { key: 'foo:index.tsx', name: 'index.tsx', qualifier: ComponentQualifier.File }, + ], + }), + ancestors: [baseComponent], + children: [], + }, + { + component: mockComponent({ + key: 'foo:test1.js', + name: 'test1.js', + path: 'test1.js', + qualifier: ComponentQualifier.File, + breadcrumbs: [ + ...baseComponent.breadcrumbs, + { key: 'foo:test1.js', name: 'test1.js', qualifier: ComponentQualifier.File }, + ], + }), + ancestors: [baseComponent], + children: [], + }, + { + component: mockComponent({ + key: 'foo:test2.js', + name: 'test2.js', + path: 'test2.js', + qualifier: ComponentQualifier.File, + breadcrumbs: [ + ...baseComponent.breadcrumbs, + { key: 'foo:test2.js', name: 'test2.js', qualifier: ComponentQualifier.File }, + ], + }), + ancestors: [baseComponent], + children: [], + }, + { + component: mockComponent({ + key: 'foo:testSymb.tsx', + name: 'testSymb.tsx', + path: 'testSymb.tsx', + qualifier: ComponentQualifier.File, + breadcrumbs: [ + ...baseComponent.breadcrumbs, + { + key: 'foo:testSymb.tsx', + name: 'testSymb.tsx', + qualifier: ComponentQualifier.File, + }, + ], + }), + ancestors: [baseComponent], + children: [], + }, + { + component: mockComponent({ + key: 'foo:empty.js', + name: 'empty.js', + path: 'empty.js', + qualifier: ComponentQualifier.File, + breadcrumbs: [ + ...baseComponent.breadcrumbs, + { key: 'foo:empty.js', name: 'empty.js', qualifier: ComponentQualifier.File }, + ], + }), + ancestors: [baseComponent], + children: [], + }, + { + component: mockComponent({ + key: 'foo:huge.js', + name: 'huge.js', + path: 'huge.js', + qualifier: ComponentQualifier.File, + breadcrumbs: [ + ...baseComponent.breadcrumbs, + { key: 'foo:huge.js', name: 'huge.js', qualifier: ComponentQualifier.File }, + ], + }), + ancestors: [baseComponent], + children: [], + }, + ], + }, + ]; + + this.defaultSourceFiles = + sourceFiles || + ([ + { + component: mockSourceViewerFile('index.tsx', 'foo'), + lines: times(50, (n) => + mockSourceLine({ + line: n, + code: 'function Test() {}', + }) + ), + }, + { + component: mockSourceViewerFile('folderA/out.tsx', 'foo'), + lines: times(50, (n) => + mockSourceLine({ + line: n, + code: 'function Test() {}', + }) + ), + }, + { + component: mockSourceViewerFile('test1.js', 'foo'), + lines: [ + { + line: 1, + code: '\u003cspan class\u003d"cd"\u003e/*\u003c/span\u003e', + scmRevision: 'f09ee6b610528aa37b7b51be395c93524cebae8f', + scmAuthor: 'stas.vilchik@sonarsource.com', + scmDate: '2018-07-10T20:21:20+0200', + duplicated: false, + isNew: false, + lineHits: 1, + coveredConditions: 1, + }, + { + line: 2, + code: '\u003cspan class\u003d"cd"\u003e * SonarQube\u003c/span\u003e', + scmRevision: 'f09ee6b610528aa37b7b51be395c93524cebae8f', + scmAuthor: 'stas.vilchik@sonarsource.com', + scmDate: '2018-07-10T20:21:20+0200', + duplicated: false, + isNew: false, + lineHits: 0, + conditions: 1, + }, + { + line: 3, + code: '\u003cspan class\u003d"cd"\u003e * Copyright\u003c/span\u003e', + scmRevision: '89a3d21bc28f2fa6201b5e8b1185d5358481b3dd', + scmAuthor: 'pierre.guillot@sonarsource.com', + scmDate: '2022-01-28T21:03:07+0100', + duplicated: false, + isNew: false, + lineHits: 1, + }, + { + line: 4, + code: '\u003cspan class\u003d"cd"\u003e * mailto:info AT sonarsource DOT com\u003c/span\u003e', + scmRevision: 'f09ee6b610528aa37b7b51be395c93524cebae8f', + scmAuthor: 'stas.vilchik@sonarsource.com', + duplicated: false, + isNew: false, + lineHits: 1, + conditions: 1, + coveredConditions: 1, + }, + { + line: 5, + code: '\u003cspan class\u003d"cd"\u003e * 5\u003c/span\u003e', + scmRevision: 'f04ee6b610528aa37b7b51be395c93524cebae8f', + duplicated: false, + isNew: false, + lineHits: 2, + conditions: 2, + coveredConditions: 1, + }, + { + line: 6, + code: '\u003cspan class\u003d"cd"\u003e * 6\u003c/span\u003e', + scmRevision: 'f04ee6b610528aa37b7b51be395c93524cebae8f', + duplicated: false, + isNew: false, + lineHits: 0, + }, + { + line: 7, + code: '\u003cspan class\u003d"cd"\u003e * 7\u003c/span\u003e', + scmRevision: 'f04ee6b610528aa37b7b51be395c93524cebae8f', + duplicated: true, + isNew: true, + }, + { + code: '\u003cspan class\u003d"cd"\u003e * This program is free software; you can redistribute it and/or\u003c/span\u003e', + scmRevision: 'f09ee6b610528aa37b7b51be395c93524cebae8f', + scmAuthor: 'stas.vilchik@sonarsource.com', + scmDate: '2018-07-10T20:21:20+0200', + duplicated: false, + isNew: false, + }, + ], + duplication: { + duplications: [ + mockDuplication({ + blocks: [ + mockDuplicationBlock({ from: 7, size: 1, _ref: '1' }), + mockDuplicationBlock({ from: 1, size: 1, _ref: '2' }), + ], + }), + ], + files: { + '1': mockDuplicatedFile({ key: 'foo:test1.js' }), + '2': mockDuplicatedFile({ key: 'foo:test2.js' }), + }, + }, + }, + { + component: mockSourceViewerFile('test2.js', 'foo'), + lines: times(50, (n) => + mockSourceLine({ + line: n, + code: `\u003cspan class\u003d"cd"\u003eLine ${n}\u003c/span\u003e`, + }) + ), + }, + { + component: mockSourceViewerFile('empty.js', 'foo'), + lines: [], + }, + { + component: mockSourceViewerFile('huge.js', 'foo'), + lines: times(200, (n) => + mockSourceLine({ + line: n, + code: `\u003cspan class\u003d"cd"\u003eLine ${n}\u003c/span\u003e`, + }) + ), + }, + { + component: mockSourceViewerFile('testSymb.tsx', 'foo'), + lines: times(20, (n) => + mockSourceLine({ + line: n, + code: ' symbole', + }) + ), + }, + ] as SourceFile[]); + + this.components = cloneDeep(this.defaultComponents); + this.sourceFiles = cloneDeep(this.defaultSourceFiles); + + (getComponentTree as jest.Mock).mockImplementation(this.handleGetComponentTree); + (getChildren as jest.Mock).mockImplementation(this.handleGetChildren); + (getTree as jest.Mock).mockImplementation(this.handleGetTree); + (getComponentData as jest.Mock).mockImplementation(this.handleGetComponentData); + (getComponentForSourceViewer as jest.Mock).mockImplementation( + this.handleGetComponentForSourceViewer + ); + (getDuplications as jest.Mock).mockImplementation(this.handleGetDuplications); + (getSources as jest.Mock).mockImplementation(this.handleGetSources); + } + + findComponentTree = (key: string, from?: ComponentTree): ComponentTree | undefined => { + const recurse = (node: ComponentTree): ComponentTree | undefined => { + if (node.component.key === key) { + return node; + } + return node.children.find((child) => recurse(child)); + }; + + if (from === undefined) { + for (let i = 0, len = this.components.length; i < len; i++) { + const tree = recurse(this.components[i]); + if (tree) { + return tree; + } + } + throw new Error(`Couldn't find component tree for key ${key}`); + } + + return recurse(from); + }; + + findSourceFile = (key: string): SourceFile => { + const sourceFile = this.sourceFiles.find((s) => s.component.key === key); + if (sourceFile) { + return sourceFile; + } + throw new Error(`Couldn't find source file for key ${key}`); + }; + + registerComponent = (component: Component, ancestors: Component[] = []) => { + this.components.push({ component, ancestors, children: [] }); + }; + + registerComponentTree = (componentTree: ComponentTree, replace = true) => { + if (replace) { + this.components = []; + } + this.components.push(componentTree); + }; + + setFailLoadingComponentStatus = (status: HttpStatus.Forbidden | HttpStatus.NotFound) => { + this.failLoadingComponentStatus = status; + }; + + getHugeFileKey = () => { + const { sourceFile } = this.sourceFiles.reduce( + (acc, sourceFile) => { + if (sourceFile.lines.length > acc.size) { + return { + sourceFile, + size: sourceFile.lines.length, + }; + } + return acc; + }, + { sourceFile: undefined, size: -Infinity } + ); + if (sourceFile) { + return sourceFile.component.key; + } + throw new Error('Could not find a large source file'); + }; + + getEmptyFileKey = () => { + const sourceFile = this.sourceFiles.find((sourceFile) => { + if (sourceFile.lines.length === 0) { + return sourceFile; + } + }); + if (sourceFile) { + return sourceFile.component.key; + } + throw new Error('Could not find an empty source file'); + }; + + getNonEmptyFileKey = (preferredKey = 'foo:test1.js') => { + let sourceFile = this.sourceFiles.find((sourceFile) => { + if (sourceFile.component.key === preferredKey) { + return sourceFile; + } + }); + + if (!sourceFile) { + sourceFile = this.sourceFiles.find((sourceFile) => { + if (sourceFile.lines.length > 0) { + return sourceFile; + } + }); + } + + if (sourceFile) { + return sourceFile.component.key; + } + throw new Error('Could not find a non-empty source file'); + }; + + reset = () => { + this.components = cloneDeep(this.defaultComponents); + this.sourceFiles = cloneDeep(this.defaultSourceFiles); + }; + + handleGetChildren = ( + component: string, + metrics: string[] = [], + data: RequestData = {} + ): Promise<{ + baseComponent: ComponentMeasure; + components: ComponentMeasure[]; + metrics: Metric[]; + paging: Paging; + }> => { + return this.handleGetComponentTree('children', component, metrics, data); + }; + + handleGetComponentTree = ( + strategy: string, + key: string, + _metrics: string[] = [], + { p = 1, ps = 100 }: RequestData = {} + ): Promise<{ + baseComponent: ComponentMeasure; + components: ComponentMeasure[]; + metrics: Metric[]; + paging: Paging; + }> => { + const base = this.findComponentTree(key); + let components: Component[] = []; + if (base === undefined) { + return Promise.reject({ + errors: [{ msg: `No component has been found for id ${key}` }], + }); + } + if (strategy === 'all' || strategy === '') { + components = listAllComponent(base); + } else if (strategy === 'children') { + components = listChildComponent(base); + } else if (strategy === 'leaves') { + components = listLeavesComponent(base); + } + + const componentsMeasures: ComponentMeasure[] = components.map((c) => { + return { + measures: this.findComponentTree(c.key, base)?.measures, + ...pick(c, ['analysisDate', 'key', 'name', 'qualifier']), + }; + }); + + return this.reply({ + baseComponent: base.component, + components: componentsMeasures.slice(ps * (p - 1), ps * (p - 1) + ps), + metrics: [], + paging: { + pageSize: ps, + pageIndex: p, + total: componentsMeasures.length, + }, + }); + }; + + handleGetTree = ({ + component: key, + q = '', + qualifiers, + }: GetTreeParams & { qualifiers?: string }): Promise<{ + baseComponent: TreeComponent; + components: TreeComponent[]; + paging: Paging; + }> => { + const base = this.findComponentTree(key); + if (base === undefined) { + return Promise.reject({ + errors: [{ msg: `No component has been found for key ${key}` }], + }); + } + const components: TreeComponent[] = listAllComponent(base) + .filter(({ name, key }) => name.includes(q) || key.includes(q)) + .filter(({ qualifier }) => (qualifiers?.length ? qualifiers.includes(qualifier) : true)) + .map((c) => ({ ...c, visibility: Visibility.Public })); + + return this.reply({ + baseComponent: { ...base.component, visibility: Visibility.Public }, + components, + paging: { + pageIndex: 1, + pageSize: 100, + total: components.length, + }, + }); + }; + + handleGetComponentData = (data: { component: string } & BranchParameters) => { + if (this.failLoadingComponentStatus !== undefined) { + return Promise.reject({ status: this.failLoadingComponentStatus }); + } + const tree = this.findComponentTree(data.component); + if (tree) { + const { component, ancestors } = tree; + return this.reply({ component, ancestors }); + } + throw new Error(`Couldn't find component with key ${data.component}`); + }; + + handleGetComponentForSourceViewer = ({ component }: { component: string } & BranchParameters) => { + const sourceFile = this.findSourceFile(component); + return this.reply(sourceFile.component); + }; + + handleGetDuplications = ({ + key, + }: { key: string } & BranchParameters): Promise<{ + duplications: Duplication[]; + files: Dict; + }> => { + const { duplication } = this.findSourceFile(key); + if (duplication) { + return this.reply(duplication); + } + return this.reply({ duplications: [], files: {} }); + }; + + handleGetSources = (data: { key: string; from?: number; to?: number } & BranchParameters) => { + const { lines } = this.findSourceFile(data.key); + const from = data.from || 1; + const to = data.to || lines.length; + return this.reply(lines.slice(from - 1, to)); + }; + + reply(response: T): Promise { + return Promise.resolve(cloneDeep(response)); + } +} diff --git a/server/sonar-web/src/main/js/api/mocks/IssuesServiceMock.ts b/server/sonar-web/src/main/js/api/mocks/IssuesServiceMock.ts index 177f06606da..b61ba990b00 100644 --- a/server/sonar-web/src/main/js/api/mocks/IssuesServiceMock.ts +++ b/server/sonar-web/src/main/js/api/mocks/IssuesServiceMock.ts @@ -17,13 +17,9 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import { cloneDeep, keyBy, range, times } from 'lodash'; +import { cloneDeep, keyBy, times } from 'lodash'; import { RuleDescriptionSections } from '../../apps/coding-rules/rule'; -import { - mockSnippetsByComponent, - mockSourceLine, - mockSourceViewerFile, -} from '../../helpers/mocks/sources'; +import { mockSnippetsByComponent } from '../../helpers/mocks/sources'; import { RequestData } from '../../helpers/request'; import { getStandards } from '../../helpers/security-standard'; import { @@ -33,7 +29,6 @@ import { mockRawIssue, mockRuleDetails, } from '../../helpers/testMocks'; -import { BranchParameters } from '../../types/branch-like'; import { IssueType, RawFacet, @@ -48,10 +43,8 @@ import { RuleActivation, RuleDetails, SnippetsByComponent, - SourceViewerFile, } from '../../types/types'; import { NoticeType } from '../../types/users'; -import { getComponentForSourceViewer, getSources } from '../components'; import { addIssueComment, bulkChangeIssues, @@ -100,20 +93,15 @@ interface IssueData { export default class IssuesServiceMock { isAdmin = false; standards?: Standards; - sourceViewerFiles: SourceViewerFile[]; + defaultList: IssueData[]; list: IssueData[]; constructor() { - // Comment should have their own store as we can test better CRUD operation - this.sourceViewerFiles = [ - mockSourceViewerFile('file.foo', 'project'), - mockSourceViewerFile('file.bar', 'project'), - ]; - this.list = [ + this.defaultList = [ { issue: mockRawIssue(false, { key: 'issue11', - component: 'project:file.foo', + component: 'foo:test1.js', message: 'FlowIssue', rule: 'simpleRuleId', textRange: { @@ -128,7 +116,7 @@ export default class IssuesServiceMock { description: 'Backtracking 1', locations: [ { - component: 'project:file.foo', + component: 'foo:test1.js', msg: 'Data location 1', textRange: { startLine: 20, @@ -138,7 +126,7 @@ export default class IssuesServiceMock { }, }, { - component: 'project:file.foo', + component: 'foo:test1.js', msg: 'Data location 2', textRange: { startLine: 21, @@ -153,7 +141,7 @@ export default class IssuesServiceMock { type: FlowType.EXECUTION, locations: [ { - component: 'project:file.bar', + component: 'foo:test2.js', msg: 'Execution location 1', textRange: { startLine: 20, @@ -163,7 +151,7 @@ export default class IssuesServiceMock { }, }, { - component: 'project:file.bar', + component: 'foo:test2.js', msg: 'Execution location 2', textRange: { startLine: 22, @@ -173,7 +161,7 @@ export default class IssuesServiceMock { }, }, { - component: 'project:file.bar', + component: 'foo:test2.js', msg: 'Execution location 3', textRange: { startLine: 5, @@ -189,13 +177,13 @@ export default class IssuesServiceMock { snippets: keyBy( [ mockSnippetsByComponent( - 'file.foo', - 'project', + 'test1.js', + 'foo', times(40, (i) => i + 1) ), mockSnippetsByComponent( - 'file.bar', - 'project', + 'test2.js', + 'foo', times(40, (i) => i + 1) ), ], @@ -205,7 +193,7 @@ export default class IssuesServiceMock { { issue: mockRawIssue(false, { key: 'issue0', - component: 'project:file.foo', + component: 'foo:test1.js', message: 'Issue on file', rule: 'simpleRuleId', textRange: undefined, @@ -216,7 +204,7 @@ export default class IssuesServiceMock { { issue: mockRawIssue(false, { key: 'issue1', - component: 'project:file.foo', + component: 'foo:test1.js', message: 'Fix this', rule: 'simpleRuleId', textRange: { @@ -229,7 +217,7 @@ export default class IssuesServiceMock { { locations: [ { - component: 'project:file.foo', + component: 'foo:test1.js', msg: 'location 1', textRange: { startLine: 1, @@ -243,7 +231,7 @@ export default class IssuesServiceMock { { locations: [ { - component: 'project:file.bar', + component: 'foo:test2.js', msg: 'location 2', textRange: { startLine: 20, @@ -259,13 +247,13 @@ export default class IssuesServiceMock { snippets: keyBy( [ mockSnippetsByComponent( - 'file.foo', - 'project', + 'test1.js', + 'foo', times(40, (i) => i + 1) ), mockSnippetsByComponent( - 'file.bar', - 'project', + 'test2.js', + 'foo', times(40, (i) => i + 1) ), ], @@ -277,7 +265,7 @@ export default class IssuesServiceMock { actions: ['set_type', 'set_tags', 'comment', 'set_severity', 'assign'], transitions: ['confirm', 'resolve', 'falsepositive', 'wontfix'], key: 'issue2', - component: 'project:file.bar', + component: 'foo:test2.js', message: 'Fix that', rule: 'advancedRuleId', textRange: { @@ -291,8 +279,8 @@ export default class IssuesServiceMock { snippets: keyBy( [ mockSnippetsByComponent( - 'file.bar', - 'project', + 'test2.js', + 'foo', times(40, (i) => i + 20) ), ], @@ -302,7 +290,7 @@ export default class IssuesServiceMock { { issue: mockRawIssue(false, { key: 'issue3', - component: 'project:file.bar', + component: 'foo:test2.js', message: 'Second issue', rule: 'other', textRange: { @@ -315,8 +303,8 @@ export default class IssuesServiceMock { snippets: keyBy( [ mockSnippetsByComponent( - 'file.bar', - 'project', + 'test2.js', + 'foo', times(40, (i) => i + 20) ), ], @@ -328,7 +316,7 @@ export default class IssuesServiceMock { actions: ['set_type', 'set_tags', 'comment', 'set_severity', 'assign'], transitions: ['confirm', 'resolve', 'falsepositive', 'wontfix'], key: 'issue4', - component: 'project:file.bar', + component: 'foo:test2.js', message: 'Issue with tags', rule: 'external_eslint_repo:no-div-regex', textRange: { @@ -344,8 +332,8 @@ export default class IssuesServiceMock { snippets: keyBy( [ mockSnippetsByComponent( - 'file.bar', - 'project', + 'test2.js', + 'foo', times(40, (i) => i + 20) ), ], @@ -353,13 +341,12 @@ export default class IssuesServiceMock { ), }, ]; + + this.list = cloneDeep(this.defaultList); + (searchIssues as jest.Mock).mockImplementation(this.handleSearchIssues); (getRuleDetails as jest.Mock).mockImplementation(this.handleGetRuleDetails); (getIssueFlowSnippets as jest.Mock).mockImplementation(this.handleGetIssueFlowSnippets); - (getSources as jest.Mock).mockImplementation(this.handleGetSources); - (getComponentForSourceViewer as jest.Mock).mockImplementation( - this.handleGetComponentForSourceViewer - ); (bulkChangeIssues as jest.Mock).mockImplementation(this.handleBulkChangeIssues); (getCurrentUser as jest.Mock).mockImplementation(this.handleGetCurrentUser); (dismissNotice as jest.Mock).mockImplementation(this.handleDismissNotification); @@ -375,6 +362,10 @@ export default class IssuesServiceMock { (searchIssueTags as jest.Mock).mockImplementation(this.handleSearchIssueTags); } + reset = () => { + this.list = cloneDeep(this.defaultList); + }; + async getStandards(): Promise { if (this.standards) { return this.standards; @@ -404,21 +395,6 @@ export default class IssuesServiceMock { return this.reply({}); }; - handleGetSources = (data: { key: string; from?: number; to?: number } & BranchParameters) => { - return this.reply(range(data.from || 1, data.to || 10).map((line) => mockSourceLine({ line }))); - }; - - handleGetComponentForSourceViewer = (data: { component: string } & BranchParameters) => { - const file = this.sourceViewerFiles.find((f) => f.key === data.component); - if (file === undefined) { - return Promise.reject({ - errors: [{ msg: `No source file has been found for id ${data.component}` }], - }); - } - - return this.reply(file); - }; - handleGetIssueFlowSnippets = (issueKey: string): Promise> => { const issue = this.list.find((i) => i.issue.key === issueKey); if (issue === undefined) { @@ -622,10 +598,14 @@ export default class IssuesServiceMock { }; getActionsResponse = (overrides: Partial, issueKey: string) => { - const issueDataSelected = this.list.find((l) => l.issue.key === issueKey)!; + const issueDataSelected = this.list.find((l) => l.issue.key === issueKey); + + if (!issueDataSelected) { + throw new Error(`Coulnd't find issue for key ${issueKey}`); + } issueDataSelected.issue = { - ...issueDataSelected?.issue, + ...issueDataSelected.issue, ...overrides, }; diff --git a/server/sonar-web/src/main/js/api/mocks/SourceViewerServiceMock.ts b/server/sonar-web/src/main/js/api/mocks/SourceViewerServiceMock.ts deleted file mode 100644 index 90ff8c7098d..00000000000 --- a/server/sonar-web/src/main/js/api/mocks/SourceViewerServiceMock.ts +++ /dev/null @@ -1,286 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2022 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. - */ -/* eslint-disable no-template-curly-in-string */ - -import { cloneDeep, pick, times } from 'lodash'; -import { - getComponentData, - getComponentForSourceViewer, - getDuplications, - getSources, -} from '../../api/components'; -import { mockSourceLine } from '../../helpers/mocks/sources'; -import { HttpStatus } from '../../helpers/request'; -import { BranchParameters } from '../../types/branch-like'; -import { Dict } from '../../types/types'; -import { setIssueSeverity } from '../issues'; - -function mockSourceFileView(name: string, project = 'project') { - return { - component: { - key: `${project}:${name}`, - name, - qualifier: 'FIL', - path: name, - language: 'js', - analysisDate: '2019-08-08T12:15:12+0200', - leakPeriodDate: '2018-08-07T11:22:22+0200', - version: '1.2-SNAPSHOT', - needIssueSync: false, - }, - sourceFileView: { - key: `${project}:${name}`, - uuid: `AWMgNpveti8CNlpVyHAm${project}${name}`, - path: name, - name, - longName: name, - q: 'FIL', - project, - projectName: 'Test project', - fav: false, - canMarkAsFavorite: true, - measures: { lines: '0', issues: '0' }, - }, - }; -} - -const ANCESTORS = [ - { - key: 'project', - name: 'Test project', - description: '', - qualifier: 'TRK', - analysisDate: '2019-08-08T12:15:12+0200', - tags: [], - visibility: 'public', - leakPeriodDate: '2018-08-07T11:22:22+0200', - version: '1.2-SNAPSHOT', - needIssueSync: false, - }, -]; -const FILES: Dict = { - 'project:test3.js': { - ...mockSourceFileView('test3.js'), - sources: [], - ancestors: ANCESTORS, - }, - 'project:test2.js': { - ...mockSourceFileView('test2.js'), - sources: times(200, (n) => - mockSourceLine({ - line: n, - code: `\u003cspan class\u003d"cd"\u003eLine ${n}\u003c/span\u003e`, - }) - ), - ancestors: ANCESTORS, - }, - 'foo:index.tsx': { - ...mockSourceFileView('index.tsx', 'foo'), - sources: times(200, (n) => - mockSourceLine({ - line: n, - code: 'function Test() {}', - }) - ), - ancestors: ANCESTORS, - }, - 'project:testSymb.tsx': { - ...mockSourceFileView('testSymb.tsx'), - sources: times(20, (n) => - mockSourceLine({ - line: n, - code: ' symbole', - }) - ), - ancestors: ANCESTORS, - }, - 'project:test.js': { - ...mockSourceFileView('test.js'), - sources: [ - { - line: 1, - code: '\u003cspan class\u003d"cd"\u003e/*\u003c/span\u003e', - scmRevision: 'f09ee6b610528aa37b7b51be395c93524cebae8f', - scmAuthor: 'stas.vilchik@sonarsource.com', - scmDate: '2018-07-10T20:21:20+0200', - duplicated: false, - isNew: false, - lineHits: 1, - coveredConditions: 1, - }, - { - line: 2, - code: '\u003cspan class\u003d"cd"\u003e * SonarQube\u003c/span\u003e', - scmRevision: 'f09ee6b610528aa37b7b51be395c93524cebae8f', - scmAuthor: 'stas.vilchik@sonarsource.com', - scmDate: '2018-07-10T20:21:20+0200', - duplicated: false, - isNew: false, - lineHits: 0, - conditions: 1, - }, - { - line: 3, - code: '\u003cspan class\u003d"cd"\u003e * Copyright\u003c/span\u003e', - scmRevision: '89a3d21bc28f2fa6201b5e8b1185d5358481b3dd', - scmAuthor: 'pierre.guillot@sonarsource.com', - scmDate: '2022-01-28T21:03:07+0100', - duplicated: false, - isNew: false, - lineHits: 1, - }, - { - line: 4, - code: '\u003cspan class\u003d"cd"\u003e * mailto:info AT sonarsource DOT com\u003c/span\u003e', - scmRevision: 'f09ee6b610528aa37b7b51be395c93524cebae8f', - scmAuthor: 'stas.vilchik@sonarsource.com', - duplicated: false, - isNew: false, - lineHits: 1, - conditions: 1, - coveredConditions: 1, - }, - { - line: 5, - code: '\u003cspan class\u003d"cd"\u003e * 5\u003c/span\u003e', - scmRevision: 'f04ee6b610528aa37b7b51be395c93524cebae8f', - duplicated: false, - isNew: false, - lineHits: 2, - conditions: 2, - coveredConditions: 1, - }, - { - line: 6, - code: '\u003cspan class\u003d"cd"\u003e * 6\u003c/span\u003e', - scmRevision: 'f04ee6b610528aa37b7b51be395c93524cebae8f', - duplicated: false, - isNew: false, - lineHits: 0, - }, - { - line: 7, - code: '\u003cspan class\u003d"cd"\u003e * 7\u003c/span\u003e', - scmRevision: 'f04ee6b610528aa37b7b51be395c93524cebae8f', - duplicated: true, - isNew: true, - }, - { - code: '\u003cspan class\u003d"cd"\u003e * This program is free software; you can redistribute it and/or\u003c/span\u003e', - scmRevision: 'f09ee6b610528aa37b7b51be395c93524cebae8f', - scmAuthor: 'stas.vilchik@sonarsource.com', - scmDate: '2018-07-10T20:21:20+0200', - duplicated: false, - isNew: false, - }, - ], - ancestors: ANCESTORS, - duplications: [ - { - blocks: [ - { from: 7, size: 1, _ref: '1' }, - { from: 1, size: 1, _ref: '2' }, - ], - }, - ], - files: { - '1': { - key: 'project:test.js', - name: 'test.js', - uuid: 'AX8NSmj8EGYw5-dyy63J', - project: 'project', - projectUuid: 'AX7juKJqVeQLJMPyb_b-', - projectName: 'project', - }, - '2': { - key: 'project:test2.js', - name: 'test2.js', - uuid: 'BX8NSmj8EGYw5-dyy63J', - project: 'project', - projectUuid: 'AX7juKJqVeQLJMPyb_b-', - projectName: 'project', - }, - }, - }, -}; - -export class SourceViewerServiceMock { - faileLoadingComponentStatus: HttpStatus | undefined = undefined; - - constructor() { - (getComponentData as jest.Mock).mockImplementation(this.handleGetComponentData); - (getComponentForSourceViewer as jest.Mock).mockImplementation( - this.handleGetComponentForSourceViewer - ); - (getDuplications as jest.Mock).mockImplementation(this.handleGetDuplications); - (getSources as jest.Mock).mockImplementation(this.handleGetSources); - (setIssueSeverity as jest.Mock).mockImplementation(this.handleSetIssueSeverity); - } - - reset() { - this.faileLoadingComponentStatus = undefined; - } - - setFailLoadingComponentStatus(status: HttpStatus.Forbidden | HttpStatus.NotFound) { - this.faileLoadingComponentStatus = status; - } - - getHugeFile(): string { - return 'project:test2.js'; - } - - getEmptyFile(): string { - return 'project:test3.js'; - } - - getFileWithSource(): string { - return 'project:test.js'; - } - - handleGetSources = (data: { key: string; from?: number; to?: number } & BranchParameters) => { - const { sources } = FILES[data.key]; - const from = data.from || 1; - const to = data.to || sources.length; - return this.reply(sources.slice(from - 1, to)); - }; - - handleGetDuplications = (data: { key: string } & BranchParameters) => { - return this.reply(pick(FILES[data.key], ['duplications', 'files'])); - }; - - handleGetComponentForSourceViewer = (data: { component: string } & BranchParameters) => { - return this.reply(FILES[data.component]['sourceFileView']); - }; - - handleGetComponentData = (data: { component: string } & BranchParameters) => { - if (this.faileLoadingComponentStatus !== undefined) { - return Promise.reject({ status: this.faileLoadingComponentStatus }); - } - return this.reply(pick(FILES[data.component], ['component', 'ancestor'])); - }; - - handleSetIssueSeverity = () => { - return this.reply({}); - }; - - reply(response: T): Promise { - return Promise.resolve(cloneDeep(response)); - } -} diff --git a/server/sonar-web/src/main/js/apps/code/__tests__/Code-it.ts b/server/sonar-web/src/main/js/apps/code/__tests__/Code-it.ts index bef9f899cdc..67a13a5a18b 100644 --- a/server/sonar-web/src/main/js/apps/code/__tests__/Code-it.ts +++ b/server/sonar-web/src/main/js/apps/code/__tests__/Code-it.ts @@ -18,18 +18,29 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import { screen } from '@testing-library/react'; +import { screen, waitFor, within } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; -import CodeServiceMock from '../../../api/mocks/CodeServiceMocks'; -import { SourceViewerServiceMock } from '../../../api/mocks/SourceViewerServiceMock'; +import { UserEvent } from '@testing-library/user-event/dist/types/setup/setup'; +import { times } from 'lodash'; +import { act } from 'react-dom/test-utils'; +import { byRole, byText } from 'testing-library-selector'; +import ComponentsServiceMock from '../../../api/mocks/ComponentsServiceMock'; +import IssuesServiceMock from '../../../api/mocks/IssuesServiceMock'; +import { isDiffMetric } from '../../../helpers/measures'; +import { mockComponent } from '../../../helpers/mocks/component'; +import { mockMeasure } from '../../../helpers/testMocks'; import { renderAppWithComponentContext } from '../../../helpers/testReactTestingUtils'; +import { ComponentQualifier } from '../../../types/component'; +import { MetricKey } from '../../../types/metrics'; +import { Component } from '../../../types/types'; import routes from '../routes'; jest.mock('../../../api/components'); jest.mock('../../../api/issues'); - -const handler = new CodeServiceMock(); -const handlerSVM = new SourceViewerServiceMock(); +// The following 2 mocks are needed, because IssuesServiceMock mocks more than it should. +// This should be removed once IssuesServiceMock is cleaned up. +jest.mock('../../../api/rules'); +jest.mock('../../../api/users'); jest.mock('../../../components/SourceViewer/helpers/lines', () => { const lines = jest.requireActual('../../../components/SourceViewer/helpers/lines'); @@ -39,33 +50,411 @@ jest.mock('../../../components/SourceViewer/helpers/lines', () => { }; }); +const DEFAULT_LINES_LOADED = 19; +const originalScrollTo = window.scrollTo; + +const issuesHandler = new IssuesServiceMock(); +const componentsHandler = new ComponentsServiceMock(); + beforeAll(() => { - handlerSVM.reset(); + Object.defineProperty(window, 'scrollTo', { + writable: true, + value: () => { + /* noop */ + }, + }); }); -it('should list project files and folder', async () => { +afterAll(() => { + Object.defineProperty(window, 'scrollTo', { + writable: true, + value: originalScrollTo, + }); +}); + +beforeEach(() => { + issuesHandler.reset(); + componentsHandler.reset(); +}); + +it('should allow navigating through the tree', async () => { + const ui = getPageObject(userEvent.setup()); renderCode(); - expect(await screen.findByText('Foo')).toBeInTheDocument(); - expect(screen.getByText('folderA')).toBeInTheDocument(); - expect(screen.getByText('index.tsx')).toBeInTheDocument(); + await ui.appLoaded(); + + // Navigate by clicking on an element. + await ui.clickOnChildComponent(/folderA$/); + expect(await ui.childComponent(/out\.tsx/).find()).toBeInTheDocument(); + + // Navigate back using the breadcrumb. + await ui.clickOnBreadcrumb(/Foo$/); + expect(await ui.childComponent(/folderA/).find()).toBeInTheDocument(); + + // Open "index.tsx" file using keyboard navigation. + await ui.arrowDown(); + await ui.arrowDown(); + await act(async () => { + await ui.arrowRight(); + // Load source viewer. + expect((await ui.sourceCode.findAll()).length).toEqual(DEFAULT_LINES_LOADED); + }); + + // Navigate back using keyboard. + await act(async () => { + await ui.arrowLeft(); + }); + expect(await ui.childComponent(/folderA/).find()).toBeInTheDocument(); }); -it('should dive into folder', async () => { - const user = userEvent.setup(); +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.childComponent(/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 act(async () => { + await ui.arrowRight(); + // Load source viewer. + expect((await ui.sourceCode.findAll()).length).toEqual(DEFAULT_LINES_LOADED); + }); + + // Navigate back using keyboard. + await act(async () => { + await ui.arrowLeft(); + }); + expect(await ui.childComponent(/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 }); - await user.click(await screen.findByText('folderA')); - expect(screen.getByText('out.tsx')).toBeInTheDocument(); + expect(await ui.componentIsEmptyTxt(qualifier).find()).toBeInTheDocument(); }); -it('should show source code', async () => { - const user = userEvent.setup(); +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, + }), + measures: generateMeasures('2.0'), + ancestors: [component], + children: [], + }, + { + component: mockComponent({ + key: 'index.tsx', + name: 'index.tsx', + }), + measures: [], + ancestors: [component], + children: [], + }, + ], + }); + const ui = getPageObject(userEvent.setup()); renderCode(); + await ui.appLoaded(component.name); + + // Folder A + const folderRow = ui.measureRow(/folderA/).get(); + [ + ['ncloc', '2'], + ['bugs', '2'], + ['vulnerabilities', '2'], + ['code_smells', '2'], + ['security_hotspots', '2'], + ['coverage', '2.0%'], + ['duplicated_lines_density', '2.0%'], + ].forEach(([domain, value]) => { + expect(ui.measureValueCell(folderRow, domain, value, 1)).toBeInTheDocument(); + }); + + // index.tsx + const fileRow = ui.measureRow(/index\.tsx/).get(); + [ + ['ncloc', '—'], + ['bugs', '—'], + ['vulnerabilities', '—'], + ['code_smells', '—'], + ['security_hotspots', '—'], + ['coverage', '—'], + ['duplicated_lines_density', '—'], + ].forEach(([domain, value]) => { + expect(ui.measureValueCell(fileRow, domain, value, 1)).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, + measures: generateMeasures('1.0', '2.0'), + ancestors: [], + children: [ + { + component: mockComponent({ + key: 'child1', + name: 'Child 1', + }), + measures: generateMeasures('2.0', '3.0'), + ancestors: [component], + children: [], + }, + { + component: mockComponent({ + key: 'child2', + name: 'Child 2', + }), + measures: [ + mockMeasure({ metric: MetricKey.alert_status, value: 'ERROR', period: undefined }), + ], + ancestors: [component], + children: [], + }, + ], + }); + 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/).get(); + [ + ['Releasability', 'OK'], + ['Reliability', 'C'], + ['vulnerabilities', 'C'], + ['security_hotspots', 'C'], + ['Maintainability', 'C'], + ['ncloc', '3'], + ].forEach(([domain, value]) => { + expect(ui.measureValueCell(child1Row, domain, value)).toBeInTheDocument(); + }); + + // Child 2 + let child2Row = ui.measureRow(/^Child 2/).get(); + [ + ['Releasability', 'ERROR'], + ['Reliability', '—'], + ['vulnerabilities', '—'], + ['security_hotspots', '—'], + ['Maintainability', '—'], + ['ncloc', '—'], + ].forEach(([domain, value]) => { + expect(ui.measureValueCell(child2Row, domain, value)).toBeInTheDocument(); + }); + + // Overall code measures + await ui.showOverallCode(); + + // Child 1 + child1Row = ui.measureRow(/^Child 1/).get(); + [ + ['Releasability', 'OK'], + ['Reliability', 'B'], + ['vulnerabilities', 'B'], + ['security_hotspots', 'B'], + ['Maintainability', 'B'], + ['ncloc', '2'], + ].forEach(([domain, value]) => { + expect(ui.measureValueCell(child1Row, domain, value)).toBeInTheDocument(); + }); - await user.click(await screen.findByText('index.tsx')); - expect(screen.getAllByText('function Test() {}')).toHaveLength(20); + // Child 2 + child2Row = ui.measureRow(/^Child 2/).get(); + [ + ['Releasability', 'ERROR'], + ['Reliability', '—'], + ['vulnerabilities', '—'], + ['security_hotspots', '—'], + ['Maintainability', '—'], + ['ncloc', '—'], + ].forEach(([domain, value]) => { + expect(ui.measureValueCell(child2Row, domain, value)).toBeInTheDocument(); + }); }); -function renderCode() { - renderAppWithComponentContext('code', routes, {}, { component: handler.getRootComponent() }); +function getPageObject(user: UserEvent) { + const ui = { + componentName: (name: string) => byText(name), + childComponent: (name: string | RegExp) => byRole('cell', { name, exact: false }), + 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('button', { name: 'projects.view.new_code' }), + overallCodeBtn: byRole('button', { name: 'projects.view.overall_code' }), + measureRow: (name: string | RegExp) => byRole('row', { name, exact: false }), + measureValueCell: (row: HTMLElement, name: string, value: string, offset = 0) => { + 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 { getAllByRole } = within(row); + const cell = getAllByRole('cell').at(i + offset); + if (cell?.textContent === value) { + return cell; + } + + // eslint-disable-next-line testing-library/no-debugging-utils + screen.debug(screen.getByRole('table'), 40000); + throw new Error(`Couldn't locate cell with value ${value} for header ${name}`); + }, + }; + + 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 [ + ...Object.values(MetricKey) + .filter((metric) => metric !== MetricKey.alert_status) + .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, + }), + ]; +} + +function renderCode({ + component = componentsHandler.findComponentTree('foo')?.component, + navigateTo, +}: { component?: Component; navigateTo?: string } = {}) { + return renderAppWithComponentContext('code', routes, { navigateTo }, { component }); } diff --git a/server/sonar-web/src/main/js/apps/code/components/CodeApp.tsx b/server/sonar-web/src/main/js/apps/code/components/CodeApp.tsx index bff1dbf41a4..c938acfe294 100644 --- a/server/sonar-web/src/main/js/apps/code/components/CodeApp.tsx +++ b/server/sonar-web/src/main/js/apps/code/components/CodeApp.tsx @@ -1,4 +1,3 @@ -/* eslint-disable no-console */ /* * SonarQube * Copyright (C) 2009-2022 SonarSource SA @@ -32,11 +31,11 @@ import ListFooter from '../../../components/controls/ListFooter'; import Suggestions from '../../../components/embed-docs-modal/Suggestions'; import { Location, Router, withRouter } from '../../../components/hoc/withRouter'; import { Alert } from '../../../components/ui/Alert'; -import { isPullRequest, isSameBranchLike } from '../../../helpers/branch-like'; +import { isPullRequest } from '../../../helpers/branch-like'; import { translate } from '../../../helpers/l10n'; import { CodeScope, getCodeUrl, getProjectUrl } from '../../../helpers/urls'; import { BranchLike } from '../../../types/branch-like'; -import { isPortfolioLike } from '../../../types/component'; +import { ComponentQualifier, isPortfolioLike } from '../../../types/component'; import { Breadcrumb, Component, ComponentMeasure, Dict, Issue, Metric } from '../../../types/types'; import { addComponent, addComponentBreadcrumbs, clearBucket } from '../bucket'; import '../code.css'; @@ -95,12 +94,7 @@ export class CodeApp extends React.Component { } componentDidUpdate(prevProps: Props) { - if ( - prevProps.component !== this.props.component || - !isSameBranchLike(prevProps.branchLike, this.props.branchLike) - ) { - this.handleComponentChange(); - } else if (prevProps.location !== this.props.location) { + if (prevProps.location.query.selected !== this.props.location.query.selected) { this.handleUpdate(); } } @@ -119,7 +113,11 @@ export class CodeApp extends React.Component { this.props.branchLike ).then((r) => { if (this.mounted) { - if (['FIL', 'UTS'].includes(r.component.qualifier)) { + if ( + [ComponentQualifier.File, ComponentQualifier.TestFile].includes( + r.component.qualifier as ComponentQualifier + ) + ) { this.setState({ breadcrumbs: r.breadcrumbs, components: r.components, @@ -266,9 +264,10 @@ export class CodeApp extends React.Component { const showSearch = searchResults !== undefined; - const hasComponents = components.length === 0 && searchResults === undefined; + const hasComponents = components.length > 0 || searchResults !== undefined; const shouldShowBreadcrumbs = breadcrumbs.length > 1 && !showSearch; + const shouldShowComponentList = sourceViewer === undefined && components.length > 0 && !showSearch; @@ -284,7 +283,12 @@ export class CodeApp extends React.Component { const metrics = metricKeys.map((metric) => this.props.metrics[metric]); const defaultTitle = - baseComponent && ['APP', 'VW', 'SVW'].includes(baseComponent.qualifier) + baseComponent && + [ + ComponentQualifier.Application, + ComponentQualifier.Portfolio, + ComponentQualifier.SubPortfolio, + ].includes(baseComponent.qualifier as ComponentQualifier) ? translate('projects.page') : translate('code.page'); @@ -308,7 +312,7 @@ export class CodeApp extends React.Component { defer={false} title={sourceViewer !== undefined ? sourceViewer.name : defaultTitle} /> - {!hasComponents && ( + {hasComponents && ( { )}
- {hasComponents && sourceViewer === undefined && ( + {!hasComponents && sourceViewer === undefined && (
{translate( diff --git a/server/sonar-web/src/main/js/apps/code/components/Search.tsx b/server/sonar-web/src/main/js/apps/code/components/Search.tsx index 7aeebf42e5f..ac199c9e19d 100644 --- a/server/sonar-web/src/main/js/apps/code/components/Search.tsx +++ b/server/sonar-web/src/main/js/apps/code/components/Search.tsx @@ -28,6 +28,7 @@ import { getBranchLikeQuery } from '../../../helpers/branch-like'; import { KeyboardKeys } from '../../../helpers/keycodes'; import { translate } from '../../../helpers/l10n'; import { BranchLike } from '../../../types/branch-like'; +import { ComponentQualifier } from '../../../types/component'; import { ComponentMeasure } from '../../../types/types'; interface Props { @@ -90,13 +91,21 @@ export class Search extends React.PureComponent { if (this.mounted) { const { branchLike, component, router, location } = this.props; this.setState({ loading: true }); - router.replace({ - pathname: location.pathname, - query: { ...location.query, search: query }, - }); - const isPortfolio = ['VW', 'SVW', 'APP'].includes(component.qualifier); - const qualifiers = isPortfolio ? 'SVW,TRK' : 'UTS,FIL'; + if (query !== location.query.search) { + router.replace({ + pathname: location.pathname, + query: { ...location.query, search: query }, + }); + } + + const qualifiers = [ + ComponentQualifier.Portfolio, + ComponentQualifier.SubPortfolio, + ComponentQualifier.Application, + ].includes(component.qualifier as ComponentQualifier) + ? [ComponentQualifier.SubPortfolio, ComponentQualifier.Project].join(',') + : [ComponentQualifier.TestFile, ComponentQualifier.File].join(','); getTree({ component: component.key, diff --git a/server/sonar-web/src/main/js/apps/code/components/SourceViewerWrapper.tsx b/server/sonar-web/src/main/js/apps/code/components/SourceViewerWrapper.tsx index 58a88ad11f2..70c0569bd2b 100644 --- a/server/sonar-web/src/main/js/apps/code/components/SourceViewerWrapper.tsx +++ b/server/sonar-web/src/main/js/apps/code/components/SourceViewerWrapper.tsx @@ -21,11 +21,10 @@ import * as React from 'react'; import withKeyboardNavigation from '../../../components/hoc/withKeyboardNavigation'; import { Location } from '../../../components/hoc/withRouter'; import SourceViewer from '../../../components/SourceViewer/SourceViewer'; -import { scrollToElement } from '../../../helpers/scrolling'; import { BranchLike } from '../../../types/branch-like'; import { Issue, Measure } from '../../../types/types'; -interface Props { +interface SourceViewerWrapperProps { branchLike?: BranchLike; component: string; componentMeasures: Measure[] | undefined; @@ -33,37 +32,32 @@ interface Props { onIssueChange?: (issue: Issue) => void; } -export class SourceViewerWrapper extends React.PureComponent { - scrollToLine = () => { - const { location } = this.props; - const { line } = location.query; +export function SourceViewerWrapper(props: SourceViewerWrapperProps) { + const { branchLike, component, componentMeasures, location } = props; + const { line } = location.query; + const finalLine = line ? Number(line) : undefined; + const handleLoaded = React.useCallback(() => { if (line) { const row = document.querySelector(`.source-line[data-line-number="${line}"]`); if (row) { - scrollToElement(row, { smooth: false, bottomOffset: window.innerHeight / 2 - 60 }); + row.scrollIntoView({ block: 'center' }); } } - }; + }, [line]); - render() { - const { branchLike, component, componentMeasures, location } = this.props; - const { line } = location.query; - const finalLine = line ? Number(line) : undefined; - - return ( - - ); - } + return ( + + ); } export default withKeyboardNavigation(SourceViewerWrapper); diff --git a/server/sonar-web/src/main/js/apps/code/components/__tests__/CodeApp-test.tsx b/server/sonar-web/src/main/js/apps/code/components/__tests__/CodeApp-test.tsx deleted file mode 100644 index f32f7b18cd3..00000000000 --- a/server/sonar-web/src/main/js/apps/code/components/__tests__/CodeApp-test.tsx +++ /dev/null @@ -1,286 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2022 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 { shallow } from 'enzyme'; -import * as React from 'react'; -import { mockPullRequest } from '../../../../helpers/mocks/branch-like'; -import { mockComponent, mockComponentMeasure } from '../../../../helpers/mocks/component'; -import { mockIssue, mockLocation, mockRouter } from '../../../../helpers/testMocks'; -import { waitAndUpdate } from '../../../../helpers/testUtils'; -import { queryToSearch } from '../../../../helpers/urls'; -import { ComponentQualifier } from '../../../../types/component'; -import { loadMoreChildren, retrieveComponent } from '../../utils'; -import { CodeApp } from '../CodeApp'; - -jest.mock('../../utils', () => { - const { getCodeMetrics } = jest.requireActual('../../utils'); - return { - getCodeMetrics, - loadMoreChildren: jest.fn().mockResolvedValue({}), - retrieveComponent: jest.fn().mockResolvedValue({ - breadcrumbs: [], - component: { qualifier: 'APP' }, - components: [], - page: 0, - total: 1, - }), - retrieveComponentChildren: () => Promise.resolve(), - }; -}); - -const METRICS = { - coverage: { id: '2', key: 'coverage', type: 'PERCENT', name: 'Coverage', domain: 'Coverage' }, - new_bugs: { id: '4', key: 'new_bugs', type: 'INT', name: 'New Bugs', domain: 'Reliability' }, -}; - -beforeEach(() => { - (retrieveComponent as jest.Mock).mockClear(); -}); - -it.each([ - [ComponentQualifier.Application], - [ComponentQualifier.Project], - [ComponentQualifier.Portfolio], - [ComponentQualifier.SubPortfolio], -])('should render correclty when no sub component for %s', async (qualifier) => { - const component = { - breadcrumbs: [], - name: 'foo', - key: 'foo', - qualifier, - canBrowseAllChildProjects: true, - }; - (retrieveComponent as jest.Mock).mockResolvedValueOnce({ - breadcrumbs: [], - component, - components: [], - page: 0, - total: 1, - }); - const wrapper = shallowRender({ component }); - await waitAndUpdate(wrapper); - expect(wrapper).toMatchSnapshot(); - wrapper.instance().handleSearchResults([]); - expect(wrapper).toMatchSnapshot('no search'); - (retrieveComponent as jest.Mock).mockResolvedValueOnce({ - breadcrumbs: [], - component, - components: [mockComponent({ qualifier: ComponentQualifier.File })], - page: 0, - total: 1, - }); - wrapper.instance().loadComponent(component.key); - await waitAndUpdate(wrapper); - expect(wrapper).toMatchSnapshot('with sub component'); -}); - -it('should refresh branch status if issues are updated', async () => { - const fetchBranchStatus = jest.fn(); - const branchLike = mockPullRequest(); - const wrapper = shallowRender({ branchLike, fetchBranchStatus }); - const instance = wrapper.instance(); - await waitAndUpdate(wrapper); - - instance.handleIssueChange(mockIssue()); - expect(fetchBranchStatus).toHaveBeenCalledWith(branchLike, 'foo'); -}); - -it('should load more behave correctly', async () => { - const component1 = mockComponent(); - const component2 = mockComponent(); - (retrieveComponent as jest.Mock).mockResolvedValueOnce({ - breadcrumbs: [], - component: mockComponent(), - components: [component1], - page: 0, - total: 1, - }); - let wrapper = shallowRender(); - await waitAndUpdate(wrapper); - - (loadMoreChildren as jest.Mock).mockResolvedValueOnce({ - components: [component2], - page: 0, - total: 1, - }); - - wrapper.instance().handleLoadMore(); - expect(wrapper.state().components).toContainEqual(component1); - expect(wrapper.state().components).toContainEqual(component2); - - (retrieveComponent as jest.Mock).mockRejectedValueOnce({}); - wrapper = shallowRender(); - await waitAndUpdate(wrapper); - wrapper.instance().handleLoadMore(); - expect(wrapper.state().components).toBeUndefined(); -}); - -it('should handle go to parent correctly', async () => { - const router = mockRouter(); - (retrieveComponent as jest.Mock).mockResolvedValueOnce({ - breadcrumbs: [], - component: mockComponent(), - components: [], - page: 0, - total: 1, - }); - let wrapper = shallowRender(); - await waitAndUpdate(wrapper); - wrapper.instance().handleGoToParent(); - expect(wrapper.state().highlighted).toBeUndefined(); - - const breadcrumb = { key: 'key2', name: 'name2', qualifier: ComponentQualifier.Directory }; - (retrieveComponent as jest.Mock).mockResolvedValueOnce({ - breadcrumbs: [ - { key: 'key1', name: 'name1', qualifier: ComponentQualifier.Directory }, - breadcrumb, - ], - component: mockComponent(), - components: [], - page: 0, - total: 1, - }); - wrapper = shallowRender({ router }); - await waitAndUpdate(wrapper); - wrapper.instance().handleGoToParent(); - expect(wrapper.state().highlighted).toBe(breadcrumb); - expect(router.push).toHaveBeenCalledWith({ - pathname: '/code', - search: queryToSearch({ id: 'foo', line: undefined, selected: 'key1' }), - }); -}); - -it('should correcly display new/overall measure for portfolio', async () => { - const component1 = mockComponent({ qualifier: ComponentQualifier.Project }); - const metrics = { - reliability_rating: { - id: '2', - key: 'reliability_rating', - type: 'RATING', - name: 'reliability_rating', - domain: 'reliability_rating', - }, - new_reliability_rating: { - id: '4', - key: 'new_reliability_rating', - type: 'RATING', - name: 'new_reliability_rating', - domain: 'new_reliability_rating', - }, - }; - (retrieveComponent as jest.Mock).mockResolvedValueOnce({ - breadcrumbs: [], - component: mockComponent(), - components: [component1], - page: 0, - total: 1, - }); - - const wrapper = shallowRender({ - component: mockComponent({ - qualifier: ComponentQualifier.Portfolio, - canBrowseAllChildProjects: true, - }), - metrics, - }); - await waitAndUpdate(wrapper); - expect(wrapper.find('withKeyboardNavigation(Components)').props()).toMatchSnapshot('new metrics'); - wrapper.setState({ newCodeSelected: false }); - expect(wrapper.find('withKeyboardNavigation(Components)').props()).toMatchSnapshot( - 'overall metrics' - ); -}); - -it('should handle select correctly', () => { - const router = mockRouter(); - const wrapper = shallowRender({ router }); - wrapper.setState({ highlighted: mockComponentMeasure() }); - - wrapper.instance().handleSelect(mockComponentMeasure(true, { refKey: 'test' })); - expect(router.push).toHaveBeenCalledWith({ - pathname: '/dashboard', - search: queryToSearch({ branch: undefined, id: 'test', code_scope: 'new' }), - }); - expect(wrapper.state().highlighted).toBeUndefined(); - - wrapper.setState({ newCodeSelected: false }); - - wrapper.instance().handleSelect(mockComponentMeasure(true, { refKey: 'test' })); - expect(router.push).toHaveBeenCalledWith({ - pathname: '/dashboard', - search: queryToSearch({ branch: undefined, id: 'test', code_scope: 'overall' }), - }); - - wrapper.instance().handleSelect(mockComponentMeasure()); - expect(router.push).toHaveBeenCalledWith({ - pathname: '/code', - search: queryToSearch({ id: 'foo', line: undefined, selected: 'foo' }), - }); -}); - -it('should render a warning message when user does not have access to all projects whithin a Portfolio', async () => { - const wrapper = shallowRender({ - component: mockComponent({ - qualifier: ComponentQualifier.Portfolio, - canBrowseAllChildProjects: false, - }), - }); - await waitAndUpdate(wrapper); - expect(wrapper).toMatchSnapshot('Project page with warning'); -}); - -it.each([ - [ComponentQualifier.Portfolio, true, false], - [ComponentQualifier.Project, false, false], - [ComponentQualifier.Portfolio, false, true], -])( - 'should not render a warning message', - async ( - componentQualifier: ComponentQualifier, - canBrowseAllChildProjects: boolean, - alertIsVisible: boolean - ) => { - const wrapper = shallowRender({ - component: mockComponent({ - qualifier: componentQualifier, - canBrowseAllChildProjects, - }), - }); - await waitAndUpdate(wrapper); - expect(wrapper.find('Styled(Alert)').exists()).toBe(alertIsVisible); - } -); - -function shallowRender(props: Partial = {}) { - return shallow( - - ); -} diff --git a/server/sonar-web/src/main/js/apps/code/components/__tests__/ComponentMeasure-test.tsx b/server/sonar-web/src/main/js/apps/code/components/__tests__/ComponentMeasure-test.tsx deleted file mode 100644 index 609338ed126..00000000000 --- a/server/sonar-web/src/main/js/apps/code/components/__tests__/ComponentMeasure-test.tsx +++ /dev/null @@ -1,72 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2022 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 { shallow } from 'enzyme'; -import * as React from 'react'; -import { mockComponentMeasure } from '../../../../helpers/mocks/component'; -import { mockMeasure, mockMetric } from '../../../../helpers/testMocks'; -import ComponentMeasure from '../ComponentMeasure'; - -it('renders correctly', () => { - expect(shallowRender()).toMatchSnapshot(); -}); - -it('renders correctly for leak values', () => { - expect( - shallow( - - ) - ).toMatchSnapshot(); -}); - -it('renders correctly when component has no measures', () => { - expect( - shallowRender({ component: mockComponentMeasure(false, { measures: undefined }) }) - ).toMatchSnapshot(); -}); - -it('should render correctly when no measure matches the metric', () => { - expect(shallowRender({ metric: mockMetric({ key: 'nonexistent_key' }) })).toMatchSnapshot(); -}); - -it('should render correctly for releasability rating', () => { - expect( - shallowRender({ - component: mockComponentMeasure(false, { - measures: [mockMeasure({ metric: 'alert_status' })], - }), - metric: mockMetric({ key: 'releasability_rating' }), - }) - ).toMatchSnapshot(); -}); - -function shallowRender(overrides: Partial = {}) { - return shallow( - - ); -} diff --git a/server/sonar-web/src/main/js/apps/code/components/__tests__/ComponentName-test.tsx b/server/sonar-web/src/main/js/apps/code/components/__tests__/ComponentName-test.tsx deleted file mode 100644 index fe58c677223..00000000000 --- a/server/sonar-web/src/main/js/apps/code/components/__tests__/ComponentName-test.tsx +++ /dev/null @@ -1,131 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2022 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 { shallow } from 'enzyme'; -import * as React from 'react'; -import { mockMainBranch } from '../../../../helpers/mocks/branch-like'; -import { mockComponentMeasure } from '../../../../helpers/mocks/component'; -import { ComponentQualifier } from '../../../../types/component'; -import ComponentName, { getTooltip, Props } from '../ComponentName'; - -describe('#getTooltip', () => { - it('should correctly format component information', () => { - expect(getTooltip(mockComponentMeasure(true))).toMatchSnapshot(); - expect(getTooltip(mockComponentMeasure(true, { qualifier: 'UTS' }))).toMatchSnapshot(); - expect(getTooltip(mockComponentMeasure(true, { path: undefined }))).toMatchSnapshot(); - expect(getTooltip(mockComponentMeasure(false))).toMatchSnapshot(); - }); -}); - -describe('#ComponentName', () => { - it('should render correctly for files', () => { - expect(shallowRender()).toMatchSnapshot(); - expect(shallowRender({ canBrowse: true })).toMatchSnapshot(); - expect( - shallowRender({ rootComponent: mockComponentMeasure(false, { qualifier: 'TRK' }) }) - ).toMatchSnapshot(); - expect( - shallowRender({ rootComponent: mockComponentMeasure(false, { qualifier: 'APP' }) }) - ).toMatchSnapshot(); - expect( - shallowRender({ - component: mockComponentMeasure(true, { branch: 'foo' }), - rootComponent: mockComponentMeasure(false, { qualifier: 'APP' }), - }) - ).toMatchSnapshot(); - expect(shallowRender({ newCodeSelected: true })).toMatchSnapshot(); - expect(shallowRender({ newCodeSelected: false })).toMatchSnapshot(); - }); - - it('should render correctly for dirs', () => { - expect( - shallowRender({ - component: mockComponentMeasure(false, { name: 'src/main/ts/app', qualifier: 'DIR' }), - previous: mockComponentMeasure(false, { name: 'src/main/ts/tests', qualifier: 'DIR' }), - }) - ).toMatchSnapshot(); - expect( - shallowRender({ - component: mockComponentMeasure(false, { name: 'src', qualifier: 'DIR' }), - previous: mockComponentMeasure(false, { name: 'lib', qualifier: 'DIR' }), - }) - ).toMatchSnapshot(); - }); - - it('should render correctly for refs', () => { - expect( - shallowRender({ - component: mockComponentMeasure(false, { - branch: 'foo', - refKey: 'src/main/ts/app', - qualifier: ComponentQualifier.Project, - }), - }) - ).toMatchSnapshot(); - expect( - shallowRender({ - component: mockComponentMeasure(false, { - branch: 'foo', - refKey: 'src/main/ts/app', - qualifier: ComponentQualifier.Project, - }), - rootComponent: mockComponentMeasure(false, { qualifier: ComponentQualifier.Application }), - }) - ).toMatchSnapshot(); - - expect( - shallowRender({ - component: mockComponentMeasure(false, { - refKey: 'src/main/ts/app', - qualifier: ComponentQualifier.Project, - }), - rootComponent: mockComponentMeasure(false, { qualifier: ComponentQualifier.Portfolio }), - }) - ).toMatchSnapshot(); - }); - - it.each([ - [ComponentQualifier.Application, 'refKey'], - [ComponentQualifier.Portfolio, 'refKey'], - [ComponentQualifier.SubPortfolio, 'refKey'], - [ComponentQualifier.Project, 'refKey'], - [ComponentQualifier.Project, undefined], - ])('should render breadcrumb correctly for %s', (qualifier, refKey) => { - expect( - shallowRender({ - component: mockComponentMeasure(false, { - refKey, - qualifier, - }), - unclickable: true, - }) - ).toMatchSnapshot(); - }); -}); - -function shallowRender(props: Partial = {}) { - return shallow( - - ); -} diff --git a/server/sonar-web/src/main/js/apps/code/components/__tests__/Components-test.tsx b/server/sonar-web/src/main/js/apps/code/components/__tests__/Components-test.tsx deleted file mode 100644 index 6e9f30e3031..00000000000 --- a/server/sonar-web/src/main/js/apps/code/components/__tests__/Components-test.tsx +++ /dev/null @@ -1,82 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2022 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 { shallow } from 'enzyme'; -import * as React from 'react'; -import { mockBranch } from '../../../../helpers/mocks/branch-like'; -import { ComponentQualifier } from '../../../../types/component'; -import { Components } from '../Components'; - -const COMPONENT = { - key: 'foo', - name: 'Foo', - qualifier: ComponentQualifier.Project, - branch: 'develop', -}; -const PORTFOLIO = { key: 'bar', name: 'Bar', qualifier: ComponentQualifier.Portfolio }; -const METRICS = [{ id: '1', key: 'coverage', type: 'PERCENT', name: 'Coverage' }]; -const BRANCH = mockBranch({ name: 'feature' }); - -it('renders correctly', () => { - expect( - shallow( - - ) - ).toMatchSnapshot(); -}); - -it('renders correctly for a search', () => { - expect( - shallow() - ).toMatchSnapshot(); -}); - -it('renders correctly for leak', () => { - expect( - shallow( - - ) - ).toMatchSnapshot(); -}); - -it('handle no components correctly', () => { - expect( - shallow( - - ) - .find('ComponentsEmpty') - .exists() - ).toBe(true); -}); diff --git a/server/sonar-web/src/main/js/apps/code/components/__tests__/ComponentsHeader-test.tsx b/server/sonar-web/src/main/js/apps/code/components/__tests__/ComponentsHeader-test.tsx deleted file mode 100644 index 2f87927dac2..00000000000 --- a/server/sonar-web/src/main/js/apps/code/components/__tests__/ComponentsHeader-test.tsx +++ /dev/null @@ -1,47 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2022 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 { shallow } from 'enzyme'; -import * as React from 'react'; -import { mockComponent } from '../../../../helpers/mocks/component'; -import ComponentsHeader from '../ComponentsHeader'; - -it('renders correctly for projects', () => { - expect(shallowRender()).toMatchSnapshot(); -}); - -it('renders correctly for portfolios', () => { - const portfolio = mockComponent({ qualifier: 'VW' }); - expect(shallowRender({ baseComponent: portfolio, rootComponent: portfolio })).toMatchSnapshot(); -}); - -it('renders correctly for a search', () => { - expect(shallowRender({ baseComponent: undefined })).toMatchSnapshot(); -}); - -function shallowRender(props = {}) { - return shallow( - - ); -} diff --git a/server/sonar-web/src/main/js/apps/code/components/__tests__/Search-test.tsx b/server/sonar-web/src/main/js/apps/code/components/__tests__/Search-test.tsx deleted file mode 100644 index 4f6479fc28c..00000000000 --- a/server/sonar-web/src/main/js/apps/code/components/__tests__/Search-test.tsx +++ /dev/null @@ -1,123 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2022 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 { shallow } from 'enzyme'; -import * as React from 'react'; -import { getTree } from '../../../../api/components'; -import { mockComponent } from '../../../../helpers/mocks/component'; -import { mockLocation, mockRouter } from '../../../../helpers/testMocks'; -import { waitAndUpdate } from '../../../../helpers/testUtils'; -import { ComponentQualifier } from '../../../../types/component'; -import { Search } from '../Search'; - -jest.mock('../../../../api/components', () => { - const { mockTreeComponent, mockComponent } = jest.requireActual( - '../../../../helpers/mocks/component' - ); - - return { - getTree: jest.fn().mockResolvedValue({ - baseComponent: mockTreeComponent(), - components: [mockComponent()], - paging: { pageIndex: 0, pageSize: 5, total: 20 }, - }), - }; -}); - -it('should render correcly', () => { - expect(shallowRender()).toMatchSnapshot(); - expect( - shallowRender({ component: mockComponent({ qualifier: ComponentQualifier.Portfolio }) }) - ).toMatchSnapshot('new code toggle for portfolio'); - expect( - shallowRender({ - component: mockComponent({ qualifier: ComponentQualifier.Portfolio }), - location: mockLocation({ query: { id: 'foo', search: 'bar' } }), - }) - ).toMatchSnapshot('new code toggle for portfolio disabled'); -}); - -it('should search correct query on mount', async () => { - const onSearchResults = jest.fn(); - const wrapper = shallowRender({ - location: mockLocation({ query: { id: 'foo', search: 'bar' } }), - onSearchResults, - }); - await waitAndUpdate(wrapper); - expect(getTree).toHaveBeenCalledWith({ - component: 'my-project', - q: 'bar', - qualifiers: 'UTS,FIL', - s: 'qualifier,name', - }); - expect(onSearchResults).toHaveBeenCalledWith([ - { - breadcrumbs: [], - key: 'my-project', - name: 'MyProject', - qualifier: 'TRK', - qualityGate: { isDefault: true, key: '30', name: 'Sonar way' }, - qualityProfiles: [{ deleted: false, key: 'my-qp', language: 'ts', name: 'Sonar way' }], - tags: [], - }, - ]); -}); - -it('should handle search correctly', async () => { - const router = mockRouter(); - const onSearchClear = jest.fn(); - const wrapper = shallowRender({ router, onSearchClear }); - wrapper.instance().handleQueryChange('foo'); - await waitAndUpdate(wrapper); - expect(router.replace).toHaveBeenCalledWith({ - pathname: '/path', - query: { - search: 'foo', - }, - }); - expect(getTree).toHaveBeenCalledWith({ - component: 'my-project', - q: 'foo', - qualifiers: 'UTS,FIL', - s: 'qualifier,name', - }); - - wrapper.instance().handleQueryChange(''); - await waitAndUpdate(wrapper); - expect(router.replace).toHaveBeenCalledWith({ - pathname: '/path', - query: {}, - }); - expect(onSearchClear).toHaveBeenCalledWith(); -}); - -function shallowRender(props?: Partial) { - return shallow( - - ); -} diff --git a/server/sonar-web/src/main/js/apps/code/components/__tests__/__snapshots__/CodeApp-test.tsx.snap b/server/sonar-web/src/main/js/apps/code/components/__tests__/__snapshots__/CodeApp-test.tsx.snap deleted file mode 100644 index 7844a278b33..00000000000 --- a/server/sonar-web/src/main/js/apps/code/components/__tests__/__snapshots__/CodeApp-test.tsx.snap +++ /dev/null @@ -1,984 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`should correcly display new/overall measure for portfolio: new metrics 1`] = ` -{ - "baseComponent": { - "breadcrumbs": [], - "key": "my-project", - "name": "MyProject", - "qualifier": "TRK", - "qualityGate": { - "isDefault": true, - "key": "30", - "name": "Sonar way", - }, - "qualityProfiles": [ - { - "deleted": false, - "key": "my-qp", - "language": "ts", - "name": "Sonar way", - }, - ], - "tags": [], - }, - "branchLike": undefined, - "components": [ - { - "breadcrumbs": [], - "key": "my-project", - "name": "MyProject", - "qualifier": "TRK", - "qualityGate": { - "isDefault": true, - "key": "30", - "name": "Sonar way", - }, - "qualityProfiles": [ - { - "deleted": false, - "key": "my-qp", - "language": "ts", - "name": "Sonar way", - }, - ], - "tags": [], - }, - ], - "cycle": true, - "metrics": [ - { - "domain": "new_reliability_rating", - "id": "4", - "key": "new_reliability_rating", - "name": "new_reliability_rating", - "type": "RATING", - }, - ], - "newCodeSelected": true, - "onEndOfList": [Function], - "onGoToParent": [Function], - "onHighlight": [Function], - "onSelect": [Function], - "rootComponent": { - "breadcrumbs": [], - "canBrowseAllChildProjects": true, - "key": "my-project", - "name": "MyProject", - "qualifier": "VW", - "qualityGate": { - "isDefault": true, - "key": "30", - "name": "Sonar way", - }, - "qualityProfiles": [ - { - "deleted": false, - "key": "my-qp", - "language": "ts", - "name": "Sonar way", - }, - ], - "tags": [], - }, - "selected": undefined, -} -`; - -exports[`should correcly display new/overall measure for portfolio: overall metrics 1`] = ` -{ - "baseComponent": { - "breadcrumbs": [], - "key": "my-project", - "name": "MyProject", - "qualifier": "TRK", - "qualityGate": { - "isDefault": true, - "key": "30", - "name": "Sonar way", - }, - "qualityProfiles": [ - { - "deleted": false, - "key": "my-qp", - "language": "ts", - "name": "Sonar way", - }, - ], - "tags": [], - }, - "branchLike": undefined, - "components": [ - { - "breadcrumbs": [], - "key": "my-project", - "name": "MyProject", - "qualifier": "TRK", - "qualityGate": { - "isDefault": true, - "key": "30", - "name": "Sonar way", - }, - "qualityProfiles": [ - { - "deleted": false, - "key": "my-qp", - "language": "ts", - "name": "Sonar way", - }, - ], - "tags": [], - }, - ], - "cycle": true, - "metrics": [ - { - "domain": "reliability_rating", - "id": "2", - "key": "reliability_rating", - "name": "reliability_rating", - "type": "RATING", - }, - ], - "newCodeSelected": false, - "onEndOfList": [Function], - "onGoToParent": [Function], - "onHighlight": [Function], - "onSelect": [Function], - "rootComponent": { - "breadcrumbs": [], - "canBrowseAllChildProjects": true, - "key": "my-project", - "name": "MyProject", - "qualifier": "VW", - "qualityGate": { - "isDefault": true, - "key": "30", - "name": "Sonar way", - }, - "qualityProfiles": [ - { - "deleted": false, - "key": "my-qp", - "language": "ts", - "name": "Sonar way", - }, - ], - "tags": [], - }, - "selected": undefined, -} -`; - -exports[`should render a warning message when user does not have access to all projects whithin a Portfolio: Project page with warning 1`] = ` -
- - - - code_viewer.not_all_measures_are_shown - - - - - -
-
- - code_viewer.no_source_code_displayed_due_to_empty_analysis.VW - -
-
-
-`; - -exports[`should render correclty when no sub component for APP 1`] = ` -
- - - -
-
- - code_viewer.no_source_code_displayed_due_to_empty_analysis.APP - -
-
-
-`; - -exports[`should render correclty when no sub component for APP: no search 1`] = ` -
- - - - -
-
- -
-
-
-`; - -exports[`should render correclty when no sub component for APP: with sub component 1`] = ` -
- - - - -
-
- -
- -
-
-`; - -exports[`should render correclty when no sub component for SVW 1`] = ` -
- - - -
-
- - code_viewer.no_source_code_displayed_due_to_empty_analysis.SVW - -
-
-
-`; - -exports[`should render correclty when no sub component for SVW: no search 1`] = ` -
- - - - -
-
- -
-
-
-`; - -exports[`should render correclty when no sub component for SVW: with sub component 1`] = ` -
- - - - -
-
- -
- -
-
-`; - -exports[`should render correclty when no sub component for TRK 1`] = ` -
- - - -
-
- - code_viewer.no_source_code_displayed_due_to_empty_analysis.TRK - -
-
-
-`; - -exports[`should render correclty when no sub component for TRK: no search 1`] = ` -
- - - - -
-
- -
-
-
-`; - -exports[`should render correclty when no sub component for TRK: with sub component 1`] = ` -
- - - - -
-
- -
- -
-
-`; - -exports[`should render correclty when no sub component for VW 1`] = ` -
- - - -
-
- - code_viewer.no_source_code_displayed_due_to_empty_analysis.VW - -
-
-
-`; - -exports[`should render correclty when no sub component for VW: no search 1`] = ` -
- - - - -
-
- -
-
-
-`; - -exports[`should render correclty when no sub component for VW: with sub component 1`] = ` -
- - - - -
-
- -
- -
-
-`; diff --git a/server/sonar-web/src/main/js/apps/code/components/__tests__/__snapshots__/ComponentMeasure-test.tsx.snap b/server/sonar-web/src/main/js/apps/code/components/__tests__/__snapshots__/ComponentMeasure-test.tsx.snap deleted file mode 100644 index 18298c07a08..00000000000 --- a/server/sonar-web/src/main/js/apps/code/components/__tests__/__snapshots__/ComponentMeasure-test.tsx.snap +++ /dev/null @@ -1,33 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`renders correctly 1`] = ` - -`; - -exports[`renders correctly for leak values 1`] = ` - -`; - -exports[`renders correctly when component has no measures 1`] = ``; - -exports[`should render correctly for releasability rating 1`] = ` - -`; - -exports[`should render correctly when no measure matches the metric 1`] = ` - - — - -`; diff --git a/server/sonar-web/src/main/js/apps/code/components/__tests__/__snapshots__/ComponentName-test.tsx.snap b/server/sonar-web/src/main/js/apps/code/components/__tests__/__snapshots__/ComponentName-test.tsx.snap deleted file mode 100644 index a4ac8a570ab..00000000000 --- a/server/sonar-web/src/main/js/apps/code/components/__tests__/__snapshots__/ComponentName-test.tsx.snap +++ /dev/null @@ -1,418 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`#ComponentName should render breadcrumb correctly for APP 1`] = ` - - - - - Foo - - -`; - -exports[`#ComponentName should render breadcrumb correctly for SVW 1`] = ` - - - - - Foo - - -`; - -exports[`#ComponentName should render breadcrumb correctly for TRK 1`] = ` - - - - - Foo - - -`; - -exports[`#ComponentName should render breadcrumb correctly for TRK 2`] = ` - - - - - Foo - - -`; - -exports[`#ComponentName should render breadcrumb correctly for VW 1`] = ` - - - - - Foo - - -`; - -exports[`#ComponentName should render correctly for dirs 1`] = ` - - - - - - - src/main/ts/ - - - app - - - - -`; - -exports[`#ComponentName should render correctly for dirs 2`] = ` - - - - - src - - -`; - -exports[`#ComponentName should render correctly for files 1`] = ` - - - - - index.tsx - - -`; - -exports[`#ComponentName should render correctly for files 2`] = ` - - - - - index.tsx - - - -`; - -exports[`#ComponentName should render correctly for files 3`] = ` - - - - - index.tsx - - -`; - -exports[`#ComponentName should render correctly for files 4`] = ` - - - - - index.tsx - - -`; - -exports[`#ComponentName should render correctly for files 5`] = ` - - - - - index.tsx - - -`; - -exports[`#ComponentName should render correctly for files 6`] = ` - - - - - index.tsx - - -`; - -exports[`#ComponentName should render correctly for files 7`] = ` - - - - - index.tsx - - -`; - -exports[`#ComponentName should render correctly for refs 1`] = ` - - - - - Foo - - - -`; - -exports[`#ComponentName should render correctly for refs 2`] = ` - - - - - - Foo - - - - - - - foo - - - -`; - -exports[`#ComponentName should render correctly for refs 3`] = ` - - - - - - Foo - - - - - branches.main_branch - - -`; - -exports[`#getTooltip should correctly format component information 1`] = ` -"src/index.tsx - -foo:src/index.tsx" -`; - -exports[`#getTooltip should correctly format component information 2`] = ` -"src/index.tsx - -foo:src/index.tsx" -`; - -exports[`#getTooltip should correctly format component information 3`] = ` -"index.tsx - -foo:src/index.tsx" -`; - -exports[`#getTooltip should correctly format component information 4`] = ` -"Foo - -foo" -`; diff --git a/server/sonar-web/src/main/js/apps/code/components/__tests__/__snapshots__/Components-test.tsx.snap b/server/sonar-web/src/main/js/apps/code/components/__tests__/__snapshots__/Components-test.tsx.snap deleted file mode 100644 index 07a04fa4dfd..00000000000 --- a/server/sonar-web/src/main/js/apps/code/components/__tests__/__snapshots__/Components-test.tsx.snap +++ /dev/null @@ -1,308 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`renders correctly 1`] = ` - - - - - - - - - - - - -
-
-
-
-
- -
-`; - -exports[`renders correctly for a search 1`] = ` - - - - - - -
- -
-`; - -exports[`renders correctly for leak 1`] = ` - - - - - - - - - - - - -
-
-
-
-
- -
-`; diff --git a/server/sonar-web/src/main/js/apps/code/components/__tests__/__snapshots__/ComponentsHeader-test.tsx.snap b/server/sonar-web/src/main/js/apps/code/components/__tests__/__snapshots__/ComponentsHeader-test.tsx.snap deleted file mode 100644 index 07adc9a12e6..00000000000 --- a/server/sonar-web/src/main/js/apps/code/components/__tests__/__snapshots__/ComponentsHeader-test.tsx.snap +++ /dev/null @@ -1,94 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`renders correctly for a search 1`] = ` - - - - - - - -`; - -exports[`renders correctly for portfolios 1`] = ` - - - - - - metric_domain.Releasability - - - metric_domain.Reliability - - - portfolio.metric_domain.vulnerabilities - - - portfolio.metric_domain.security_hotspots - - - metric_domain.Maintainability - - - metric.ncloc.name - - - - -`; - -exports[`renders correctly for projects 1`] = ` - - - - - - metric.foo.name - - - metric.bar.name - - - - -`; diff --git a/server/sonar-web/src/main/js/apps/code/components/__tests__/__snapshots__/Search-test.tsx.snap b/server/sonar-web/src/main/js/apps/code/components/__tests__/__snapshots__/Search-test.tsx.snap deleted file mode 100644 index 6dc5fb62d22..00000000000 --- a/server/sonar-web/src/main/js/apps/code/components/__tests__/__snapshots__/Search-test.tsx.snap +++ /dev/null @@ -1,100 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`should render correcly 1`] = ` - -`; - -exports[`should render correcly: new code toggle for portfolio 1`] = ` - -`; - -exports[`should render correcly: new code toggle for portfolio disabled 1`] = ` - -`; diff --git a/server/sonar-web/src/main/js/apps/issues/__tests__/IssuesApp-it.tsx b/server/sonar-web/src/main/js/apps/issues/__tests__/IssuesApp-it.tsx index 2c98af2587b..1c85d92c84f 100644 --- a/server/sonar-web/src/main/js/apps/issues/__tests__/IssuesApp-it.tsx +++ b/server/sonar-web/src/main/js/apps/issues/__tests__/IssuesApp-it.tsx @@ -21,6 +21,7 @@ import { screen, within } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import React from 'react'; import selectEvent from 'react-select-event'; +import ComponentsServiceMock from '../../../api/mocks/ComponentsServiceMock'; import IssuesServiceMock from '../../../api/mocks/IssuesServiceMock'; import { TabKeys } from '../../../components/rules/RuleTabViewer'; import { mockComponent } from '../../../helpers/mocks/component'; @@ -39,10 +40,12 @@ jest.mock('../../../api/rules'); jest.mock('../../../api/components'); jest.mock('../../../api/users'); -let handler: IssuesServiceMock; +const issuesHandler = new IssuesServiceMock(); +const componentsHandler = new ComponentsServiceMock(); beforeEach(() => { - handler = new IssuesServiceMock(); + issuesHandler.reset(); + componentsHandler.reset(); window.scrollTo = jest.fn(); window.HTMLElement.prototype.scrollIntoView = jest.fn(); }); @@ -50,7 +53,7 @@ beforeEach(() => { //Improve this to include all the bulk change fonctionality it('should be able to bulk change', async () => { const user = userEvent.setup(); - handler.setIsAdmin(true); + issuesHandler.setIsAdmin(true); renderIssueApp(mockCurrentUser({ isLoggedIn: true })); // Check that the bulk button has correct behavior @@ -256,7 +259,7 @@ it('should open issue and navigate', async () => { expect(screen.getByRole('region', { name: 'Issue on file' })).toBeInTheDocument(); expect( screen.getByRole('row', { - name: '2 source_viewer.tooltip.covered import java.util. ArrayList ;', + name: '2 * SonarQube', }) ).toBeInTheDocument(); }); @@ -279,8 +282,8 @@ it('should support OWASP Top 10 version 2021', async () => { await user.click(owaspTop102021); await Promise.all( - handler.owasp2021FacetList().values.map(async ({ val }) => { - const standard = await handler.getStandards(); + issuesHandler.owasp2021FacetList().values.map(async ({ val }) => { + const standard = await issuesHandler.getStandards(); /* eslint-disable-next-line testing-library/render-result-naming-convention */ const linkName = renderOwaspTop102021Category(standard, val); expect(await screen.findByRole('checkbox', { name: linkName })).toBeInTheDocument(); @@ -290,7 +293,7 @@ it('should support OWASP Top 10 version 2021', async () => { it('should be able to perform action on issues', async () => { const user = userEvent.setup(); - handler.setIsAdmin(true); + issuesHandler.setIsAdmin(true); renderIssueApp(); // Select an issue with an advanced rule @@ -484,7 +487,7 @@ it('should not allow performing actions when user does not have permission', asy it('should open the actions popup using keyboard shortcut', async () => { const user = userEvent.setup(); - handler.setIsAdmin(true); + issuesHandler.setIsAdmin(true); renderIssueApp(); // Select an issue with an advanced rule @@ -521,7 +524,7 @@ it('should open the actions popup using keyboard shortcut', async () => { it('should not open the actions popup using keyboard shortcut when keyboard shortcut flag is disabled', async () => { localStorage.setItem('sonarqube.preferences.keyboard_shortcuts_enabled', 'false'); const user = userEvent.setup(); - handler.setIsAdmin(true); + issuesHandler.setIsAdmin(true); renderIssueApp(); // Select an issue with an advanced rule @@ -587,7 +590,7 @@ it('should show code tabs when any secondary location is selected', async () => it('should show issue tags if applicable', async () => { const user = userEvent.setup(); - handler.setIsAdmin(true); + issuesHandler.setIsAdmin(true); renderIssueApp(); // Select an issue with an advanced rule diff --git a/server/sonar-web/src/main/js/components/SourceViewer/__tests__/SourceViewer-it.tsx b/server/sonar-web/src/main/js/components/SourceViewer/__tests__/SourceViewer-it.tsx index 3e445208c1a..f31a9c80f41 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/__tests__/SourceViewer-it.tsx +++ b/server/sonar-web/src/main/js/components/SourceViewer/__tests__/SourceViewer-it.tsx @@ -20,8 +20,10 @@ import { queryHelpers, screen, within } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import * as React from 'react'; +import { act } from 'react-dom/test-utils'; import { byRole } from 'testing-library-selector'; -import { SourceViewerServiceMock } from '../../../api/mocks/SourceViewerServiceMock'; +import ComponentsServiceMock from '../../../api/mocks/ComponentsServiceMock'; +import IssuesServiceMock from '../../../api/mocks/IssuesServiceMock'; import { HttpStatus } from '../../../helpers/request'; import { mockIssue } from '../../../helpers/testMocks'; import { renderComponent } from '../../../helpers/testReactTestingUtils'; @@ -30,10 +32,16 @@ import SourceViewer from '../SourceViewer'; jest.mock('../../../api/components'); jest.mock('../../../api/issues'); +// The following 2 mocks are needed, because IssuesServiceMock mocks more than it should. +// This should be removed once IssuesServiceMock is cleaned up. +jest.mock('../../../api/rules'); +jest.mock('../../../api/users'); + jest.mock('../helpers/loadIssues', () => ({ __esModule: true, default: jest.fn().mockResolvedValue([]), })); + jest.mock('../helpers/lines', () => { const lines = jest.requireActual('../helpers/lines'); return { @@ -47,10 +55,12 @@ const ui = { minorSeverityButton: byRole('button', { name: 'severity.MINOR' }), }; -const handler = new SourceViewerServiceMock(); +const componentsHandler = new ComponentsServiceMock(); +const issuesHandler = new IssuesServiceMock(); beforeEach(() => { - handler.reset(); + issuesHandler.reset(); + componentsHandler.reset(); }); it('should show a permalink on line number', async () => { @@ -64,48 +74,46 @@ it('should show a permalink on line number', async () => { name: 'source_viewer.line_X.1', }) ); - await user.click( - rowScreen.getByRole('button', { - name: 'component_viewer.copy_permalink', - }) - ); expect( /* eslint-disable-next-line testing-library/prefer-presence-queries */ queryHelpers.queryByAttribute( 'data-clipboard-text', row, - 'http://localhost/code?id=project&selected=project%3Atest.js&line=1' + 'http://localhost/code?id=foo&selected=foo%3Atest1.js&line=1' ) ).toBeInTheDocument(); - await user.keyboard('[Escape]'); + await act(async () => { + await user.keyboard('[Escape]'); + }); expect( /* eslint-disable-next-line testing-library/prefer-presence-queries */ queryHelpers.queryByAttribute( 'data-clipboard-text', row, - 'http://localhost/code?id=project&selected=project%3Atest.js&line=1' + 'http://localhost/code?id=foo&selected=foo%3Atest1.js&line=1' ) ).not.toBeInTheDocument(); row = await screen.findByRole('row', { name: / \* 6$/ }); expect(row).toBeInTheDocument(); const lowerRowScreen = within(row); - await user.click( - lowerRowScreen.getByRole('button', { - name: 'source_viewer.line_X.6', - }) - ); + + await act(async () => { + await user.click( + lowerRowScreen.getByRole('button', { + name: 'source_viewer.line_X.6', + }) + ); + }); expect( lowerRowScreen.getByRole('button', { name: 'component_viewer.copy_permalink', }) ).toBeInTheDocument(); - - await user.keyboard('[Escape]'); }); it('should show issue on empty file', async () => { @@ -118,7 +126,7 @@ it('should show issue on empty file', async () => { }), ]); renderSourceViewer({ - component: handler.getEmptyFile(), + component: componentsHandler.getEmptyFileKey(), }); expect(await screen.findByRole('table')).toBeInTheDocument(); expect(await screen.findByRole('row', { name: 'First Issue' })).toBeInTheDocument(); @@ -128,7 +136,7 @@ it('should be able to interact with issue action', async () => { (loadIssues as jest.Mock).mockResolvedValueOnce([ mockIssue(false, { actions: ['set_type', 'set_tags', 'comment', 'set_severity', 'assign'], - key: 'first-issue', + key: 'issue1', message: 'First Issue', line: 1, textRange: { startLine: 1, endLine: 1, startOffset: 0, endOffset: 1 }, @@ -171,12 +179,12 @@ it('should be able to interact with issue action', async () => { }); it('should load line when looking arround unloaded line', async () => { - const { rerender } = renderSourceViewer({ + const rerender = renderSourceViewer({ aroundLine: 50, - component: handler.getHugeFile(), + component: componentsHandler.getHugeFileKey(), }); expect(await screen.findByRole('row', { name: /Line 50$/ })).toBeInTheDocument(); - rerender(getSourceViewerUi({ aroundLine: 100, component: handler.getHugeFile() })); + rerender({ aroundLine: 100, component: componentsHandler.getHugeFileKey() }); expect(await screen.findByRole('row', { name: /Line 100$/ })).toBeInTheDocument(); }); @@ -365,14 +373,17 @@ it('should show duplication block', async () => { duplicateLine.getByRole('button', { name: 'source_viewer.tooltip.duplicated_block' }) ); - expect(duplicateLine.getAllByRole('link', { name: 'test2.js' })[0]).toBeInTheDocument(); - await user.keyboard('[Escape]'); - expect(duplicateLine.queryByRole('link', { name: 'test2.js' })).not.toBeInTheDocument(); + expect(duplicateLine.getAllByRole('link', { name: 'foo:test2.js' })[0]).toBeInTheDocument(); + + await act(async () => { + await user.keyboard('[Escape]'); + }); + expect(duplicateLine.queryByRole('link', { name: 'foo:test2.js' })).not.toBeInTheDocument(); }); it('should highlight symbol', async () => { const user = userEvent.setup(); - renderSourceViewer({ component: 'project:testSymb.tsx' }); + renderSourceViewer({ component: 'foo:testSymb.tsx' }); const symbols = await screen.findAllByText('symbole'); await user.click(symbols[0]); @@ -383,7 +394,7 @@ it('should highlight symbol', async () => { }); it('should show correct message when component is not asscessible', async () => { - handler.setFailLoadingComponentStatus(HttpStatus.Forbidden); + componentsHandler.setFailLoadingComponentStatus(HttpStatus.Forbidden); renderSourceViewer(); expect( await screen.findByText('code_viewer.no_source_code_displayed_due_to_security') @@ -391,21 +402,17 @@ it('should show correct message when component is not asscessible', async () => }); it('should show correct message when component does not exist', async () => { - handler.setFailLoadingComponentStatus(HttpStatus.NotFound); + componentsHandler.setFailLoadingComponentStatus(HttpStatus.NotFound); renderSourceViewer(); expect(await screen.findByText('component_viewer.no_component')).toBeInTheDocument(); }); function renderSourceViewer(override?: Partial) { - return renderComponent(getSourceViewerUi(override)); -} - -function getSourceViewerUi(override?: Partial) { - return ( + const { rerender } = renderComponent( ) { {...override} /> ); + return function (reoverride?: Partial) { + rerender( + + ); + }; } diff --git a/server/sonar-web/src/main/js/components/hoc/withKeyboardNavigation.tsx b/server/sonar-web/src/main/js/components/hoc/withKeyboardNavigation.tsx index f475e8805a4..89badf3e9de 100644 --- a/server/sonar-web/src/main/js/components/hoc/withKeyboardNavigation.tsx +++ b/server/sonar-web/src/main/js/components/hoc/withKeyboardNavigation.tsx @@ -37,7 +37,7 @@ export interface WithKeyboardNavigationProps { } export default function withKeyboardNavigation

( - WrappedComponent: React.ComponentClass

> + WrappedComponent: React.ComponentType

> ) { return class Wrapper extends React.Component

{ static displayName = getWrappedDisplayName(WrappedComponent, 'withKeyboardNavigation'); diff --git a/server/sonar-web/src/main/js/helpers/mocks/sources.ts b/server/sonar-web/src/main/js/helpers/mocks/sources.ts index 0fe4db77452..87143adac82 100644 --- a/server/sonar-web/src/main/js/helpers/mocks/sources.ts +++ b/server/sonar-web/src/main/js/helpers/mocks/sources.ts @@ -19,7 +19,14 @@ */ import { ComponentQualifier } from '../../types/component'; -import { SnippetsByComponent, SourceLine, SourceViewerFile } from '../../types/types'; +import { + DuplicatedFile, + Duplication, + DuplicationBlock, + SnippetsByComponent, + SourceLine, + SourceViewerFile, +} from '../../types/types'; export function mockSourceViewerFile( name = 'foo/bar.ts', @@ -76,3 +83,28 @@ export function mockSnippetsByComponent( sources, }; } + +export function mockDuplicatedFile(overrides: Partial = {}): DuplicatedFile { + return { + key: 'file1.java', + name: overrides.key || 'file1.java', + project: 'foo', + projectName: 'Foo', + ...overrides, + }; +} + +export function mockDuplication(overrides: Partial = {}): Duplication { + return { + blocks: [mockDuplicationBlock()], + ...overrides, + }; +} + +export function mockDuplicationBlock(overrides: Partial = {}): DuplicationBlock { + return { + from: 12, + size: 5, + ...overrides, + }; +} -- 2.39.5