aboutsummaryrefslogtreecommitdiffstats
path: root/server/sonar-web
diff options
context:
space:
mode:
Diffstat (limited to 'server/sonar-web')
-rw-r--r--server/sonar-web/build.gradle2
-rw-r--r--server/sonar-web/src/main/js/api/components.ts2
-rw-r--r--server/sonar-web/src/main/js/api/mocks/CodeServiceMocks.ts168
-rw-r--r--server/sonar-web/src/main/js/api/mocks/ComponentsServiceMock.ts631
-rw-r--r--server/sonar-web/src/main/js/api/mocks/IssuesServiceMock.ts108
-rw-r--r--server/sonar-web/src/main/js/api/mocks/SourceViewerServiceMock.ts286
-rw-r--r--server/sonar-web/src/main/js/apps/code/__tests__/Code-it.ts431
-rw-r--r--server/sonar-web/src/main/js/apps/code/components/CodeApp.tsx32
-rw-r--r--server/sonar-web/src/main/js/apps/code/components/Search.tsx21
-rw-r--r--server/sonar-web/src/main/js/apps/code/components/SourceViewerWrapper.tsx46
-rw-r--r--server/sonar-web/src/main/js/apps/code/components/__tests__/CodeApp-test.tsx286
-rw-r--r--server/sonar-web/src/main/js/apps/code/components/__tests__/ComponentMeasure-test.tsx72
-rw-r--r--server/sonar-web/src/main/js/apps/code/components/__tests__/ComponentName-test.tsx131
-rw-r--r--server/sonar-web/src/main/js/apps/code/components/__tests__/Components-test.tsx82
-rw-r--r--server/sonar-web/src/main/js/apps/code/components/__tests__/ComponentsHeader-test.tsx47
-rw-r--r--server/sonar-web/src/main/js/apps/code/components/__tests__/Search-test.tsx123
-rw-r--r--server/sonar-web/src/main/js/apps/code/components/__tests__/__snapshots__/CodeApp-test.tsx.snap984
-rw-r--r--server/sonar-web/src/main/js/apps/code/components/__tests__/__snapshots__/ComponentMeasure-test.tsx.snap33
-rw-r--r--server/sonar-web/src/main/js/apps/code/components/__tests__/__snapshots__/ComponentName-test.tsx.snap418
-rw-r--r--server/sonar-web/src/main/js/apps/code/components/__tests__/__snapshots__/Components-test.tsx.snap308
-rw-r--r--server/sonar-web/src/main/js/apps/code/components/__tests__/__snapshots__/ComponentsHeader-test.tsx.snap94
-rw-r--r--server/sonar-web/src/main/js/apps/code/components/__tests__/__snapshots__/Search-test.tsx.snap100
-rw-r--r--server/sonar-web/src/main/js/apps/issues/__tests__/IssuesApp-it.tsx23
-rw-r--r--server/sonar-web/src/main/js/components/SourceViewer/__tests__/SourceViewer-it.tsx96
-rw-r--r--server/sonar-web/src/main/js/components/hoc/withKeyboardNavigation.tsx2
-rw-r--r--server/sonar-web/src/main/js/helpers/mocks/sources.ts34
26 files changed, 1248 insertions, 3312 deletions
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<RawIssuesResponse> => {
- return this.reply({
- components: [],
- effortTotal: 1,
- facets: [],
- issues: [],
- languages: [],
- paging: { total: 0, pageIndex: 1, pageSize: 100 },
- });
- };
-
- getRootComponent() {
- return mockComponent(this.componentTree.component);
- }
-
- reply<T>(response: T): Promise<T> {
- 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<DuplicatedFile>;
+ };
+}
+
+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: ' <span class="sym-35 sym">symbole</span>',
+ })
+ ),
+ },
+ ] 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<DuplicatedFile>;
+ }> => {
+ 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<T>(response: T): Promise<T> {
+ 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<Standards> {
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<Dict<SnippetsByComponent>> => {
const issue = this.list.find((i) => i.issue.key === issueKey);
if (issue === undefined) {
@@ -622,10 +598,14 @@ export default class IssuesServiceMock {
};
getActionsResponse = (overrides: Partial<RawIssue>, 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<any> = {
- '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: ' <span class="sym-35 sym">symbole</span>',
- })
- ),
- 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<T>(response: T): Promise<T> {
- 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<Props, State> {
}
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<Props, State> {
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<Props, State> {
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<Props, State> {
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<Props, State> {
defer={false}
title={sourceViewer !== undefined ? sourceViewer.name : defaultTitle}
/>
- {!hasComponents && (
+ {hasComponents && (
<Search
branchLike={branchLike}
component={component}
@@ -320,7 +324,7 @@ export class CodeApp extends React.Component<Props, State> {
)}
<div className="code-components">
- {hasComponents && sourceViewer === undefined && (
+ {!hasComponents && sourceViewer === undefined && (
<div className="display-flex-center display-flex-column no-file">
<span className="h1 text-muted">
{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<Props, State> {
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<Props> {
- 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 (
- <SourceViewer
- aroundLine={finalLine}
- branchLike={branchLike}
- component={component}
- componentMeasures={componentMeasures}
- highlightedLine={finalLine}
- onIssueChange={this.props.onIssueChange}
- onLoaded={this.scrollToLine}
- showMeasures={true}
- />
- );
- }
+ return (
+ <SourceViewer
+ aroundLine={finalLine}
+ branchLike={branchLike}
+ component={component}
+ componentMeasures={componentMeasures}
+ highlightedLine={finalLine}
+ onIssueChange={props.onIssueChange}
+ onLoaded={handleLoaded}
+ showMeasures={true}
+ />
+ );
}
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<any>).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<any>).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<any>).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<any>).mockResolvedValueOnce({
- breadcrumbs: [],
- component: mockComponent(),
- components: [component1],
- page: 0,
- total: 1,
- });
- let wrapper = shallowRender();
- await waitAndUpdate(wrapper);
-
- (loadMoreChildren as jest.Mock<any>).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<any>).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<any>).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<any>).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<any>).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<CodeApp['props']> = {}) {
- return shallow<CodeApp>(
- <CodeApp
- component={{
- breadcrumbs: [],
- name: 'foo',
- key: 'foo',
- qualifier: 'FOO',
- }}
- fetchBranchStatus={jest.fn()}
- location={mockLocation({ search: queryToSearch({ branch: 'b', id: 'foo', line: '7' }) })}
- metrics={METRICS}
- router={mockRouter()}
- {...props}
- />
- );
-}
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(
- <ComponentMeasure
- component={mockComponentMeasure(false, {
- measures: [mockMeasure({ metric: 'new_coverage' })],
- })}
- metric={mockMetric({ key: 'new_coverage', name: 'Coverage on New Code' })}
- />
- )
- ).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<ComponentMeasure['props']> = {}) {
- return shallow(
- <ComponentMeasure
- component={mockComponentMeasure(false, { measures: [mockMeasure({ metric: 'coverage' })] })}
- metric={mockMetric()}
- {...overrides}
- />
- );
-}
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<Props> = {}) {
- return shallow(
- <ComponentName
- branchLike={mockMainBranch()}
- component={mockComponentMeasure(true)}
- rootComponent={mockComponentMeasure()}
- {...props}
- />
- );
-}
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(
- <Components
- baseComponent={COMPONENT}
- components={[COMPONENT]}
- metrics={METRICS}
- rootComponent={COMPONENT}
- />
- )
- ).toMatchSnapshot();
-});
-
-it('renders correctly for a search', () => {
- expect(
- shallow(<Components components={[COMPONENT]} metrics={[]} rootComponent={COMPONENT} />)
- ).toMatchSnapshot();
-});
-
-it('renders correctly for leak', () => {
- expect(
- shallow(
- <Components
- baseComponent={COMPONENT}
- branchLike={BRANCH}
- components={[COMPONENT]}
- metrics={METRICS}
- rootComponent={COMPONENT}
- />
- )
- ).toMatchSnapshot();
-});
-
-it('handle no components correctly', () => {
- expect(
- shallow(
- <Components
- baseComponent={PORTFOLIO}
- components={[]}
- metrics={METRICS}
- rootComponent={PORTFOLIO}
- />
- )
- .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(
- <ComponentsHeader
- baseComponent={mockComponent()}
- metrics={['foo', 'bar']}
- rootComponent={mockComponent()}
- {...props}
- />
- );
-}
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<Search['props']>) {
- return shallow<Search>(
- <Search
- newCodeSelected={false}
- component={mockComponent()}
- location={mockLocation()}
- onSearchClear={jest.fn()}
- onSearchResults={jest.fn()}
- onNewCodeToggle={jest.fn()}
- router={mockRouter()}
- {...props}
- />
- );
-}
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`] = `
-<div
- className="page page-limited"
->
- <A11ySkipTarget
- anchor="code_main"
- />
- <Styled(Alert)
- className="it__portfolio_warning"
- variant="warning"
- >
- <Styled(div)>
- code_viewer.not_all_measures_are_shown
- <HelpTooltip
- ariaLabel="code_viewer.not_all_measures_are_shown.help"
- className="spacer-left"
- overlay="code_viewer.not_all_measures_are_shown.help"
- />
- </Styled(div)>
- </Styled(Alert)>
- <Suggestions
- suggestions="code"
- />
- <Helmet
- defer={false}
- encodeSpecialCharacters={true}
- prioritizeSeoTags={false}
- title="projects.page"
- />
- <div
- className="code-components"
- >
- <div
- className="display-flex-center display-flex-column no-file"
- >
- <span
- className="h1 text-muted"
- >
- code_viewer.no_source_code_displayed_due_to_empty_analysis.VW
- </span>
- </div>
- </div>
-</div>
-`;
-
-exports[`should render correclty when no sub component for APP 1`] = `
-<div
- className="page page-limited"
->
- <A11ySkipTarget
- anchor="code_main"
- />
- <Suggestions
- suggestions="code"
- />
- <Helmet
- defer={false}
- encodeSpecialCharacters={true}
- prioritizeSeoTags={false}
- title="projects.page"
- />
- <div
- className="code-components"
- >
- <div
- className="display-flex-center display-flex-column no-file"
- >
- <span
- className="h1 text-muted"
- >
- code_viewer.no_source_code_displayed_due_to_empty_analysis.APP
- </span>
- </div>
- </div>
-</div>
-`;
-
-exports[`should render correclty when no sub component for APP: no search 1`] = `
-<div
- className="page page-limited"
->
- <A11ySkipTarget
- anchor="code_main"
- />
- <Suggestions
- suggestions="code"
- />
- <Helmet
- defer={false}
- encodeSpecialCharacters={true}
- prioritizeSeoTags={false}
- title="projects.page"
- />
- <withRouter(Search)
- component={
- {
- "breadcrumbs": [],
- "canBrowseAllChildProjects": true,
- "key": "foo",
- "name": "foo",
- "qualifier": "APP",
- }
- }
- newCodeSelected={true}
- onNewCodeToggle={[Function]}
- onSearchClear={[Function]}
- onSearchResults={[Function]}
- />
- <div
- className="code-components"
- >
- <div
- className="boxed-group spacer-top search-results"
- >
- <withKeyboardNavigation(Components)
- components={[]}
- metrics={[]}
- onHighlight={[Function]}
- onSelect={[Function]}
- rootComponent={
- {
- "breadcrumbs": [],
- "canBrowseAllChildProjects": true,
- "key": "foo",
- "name": "foo",
- "qualifier": "APP",
- }
- }
- />
- </div>
- </div>
-</div>
-`;
-
-exports[`should render correclty when no sub component for APP: with sub component 1`] = `
-<div
- className="page page-limited"
->
- <A11ySkipTarget
- anchor="code_main"
- />
- <Suggestions
- suggestions="code"
- />
- <Helmet
- defer={false}
- encodeSpecialCharacters={true}
- prioritizeSeoTags={false}
- title="projects.page"
- />
- <withRouter(Search)
- component={
- {
- "breadcrumbs": [],
- "canBrowseAllChildProjects": true,
- "key": "foo",
- "name": "foo",
- "qualifier": "APP",
- }
- }
- newCodeSelected={true}
- onNewCodeToggle={[Function]}
- onSearchClear={[Function]}
- onSearchResults={[Function]}
- />
- <div
- className="code-components"
- >
- <div
- className="boxed-group spacer-top"
- >
- <withKeyboardNavigation(Components)
- baseComponent={
- {
- "breadcrumbs": [],
- "canBrowseAllChildProjects": true,
- "key": "foo",
- "name": "foo",
- "qualifier": "APP",
- }
- }
- components={
- [
- {
- "breadcrumbs": [],
- "key": "my-project",
- "name": "MyProject",
- "qualifier": "FIL",
- "qualityGate": {
- "isDefault": true,
- "key": "30",
- "name": "Sonar way",
- },
- "qualityProfiles": [
- {
- "deleted": false,
- "key": "my-qp",
- "language": "ts",
- "name": "Sonar way",
- },
- ],
- "tags": [],
- },
- ]
- }
- cycle={true}
- metrics={
- [
- {
- "domain": "Coverage",
- "id": "2",
- "key": "coverage",
- "name": "Coverage",
- "type": "PERCENT",
- },
- ]
- }
- newCodeSelected={true}
- onEndOfList={[Function]}
- onGoToParent={[Function]}
- onHighlight={[Function]}
- onSelect={[Function]}
- rootComponent={
- {
- "breadcrumbs": [],
- "canBrowseAllChildProjects": true,
- "key": "foo",
- "name": "foo",
- "qualifier": "APP",
- }
- }
- />
- </div>
- <ListFooter
- count={1}
- loadMore={[Function]}
- total={1}
- />
- </div>
-</div>
-`;
-
-exports[`should render correclty when no sub component for SVW 1`] = `
-<div
- className="page page-limited"
->
- <A11ySkipTarget
- anchor="code_main"
- />
- <Suggestions
- suggestions="code"
- />
- <Helmet
- defer={false}
- encodeSpecialCharacters={true}
- prioritizeSeoTags={false}
- title="projects.page"
- />
- <div
- className="code-components"
- >
- <div
- className="display-flex-center display-flex-column no-file"
- >
- <span
- className="h1 text-muted"
- >
- code_viewer.no_source_code_displayed_due_to_empty_analysis.SVW
- </span>
- </div>
- </div>
-</div>
-`;
-
-exports[`should render correclty when no sub component for SVW: no search 1`] = `
-<div
- className="page page-limited"
->
- <A11ySkipTarget
- anchor="code_main"
- />
- <Suggestions
- suggestions="code"
- />
- <Helmet
- defer={false}
- encodeSpecialCharacters={true}
- prioritizeSeoTags={false}
- title="projects.page"
- />
- <withRouter(Search)
- component={
- {
- "breadcrumbs": [],
- "canBrowseAllChildProjects": true,
- "key": "foo",
- "name": "foo",
- "qualifier": "SVW",
- }
- }
- newCodeSelected={true}
- onNewCodeToggle={[Function]}
- onSearchClear={[Function]}
- onSearchResults={[Function]}
- />
- <div
- className="code-components"
- >
- <div
- className="boxed-group spacer-top search-results"
- >
- <withKeyboardNavigation(Components)
- components={[]}
- metrics={[]}
- onHighlight={[Function]}
- onSelect={[Function]}
- rootComponent={
- {
- "breadcrumbs": [],
- "canBrowseAllChildProjects": true,
- "key": "foo",
- "name": "foo",
- "qualifier": "SVW",
- }
- }
- />
- </div>
- </div>
-</div>
-`;
-
-exports[`should render correclty when no sub component for SVW: with sub component 1`] = `
-<div
- className="page page-limited"
->
- <A11ySkipTarget
- anchor="code_main"
- />
- <Suggestions
- suggestions="code"
- />
- <Helmet
- defer={false}
- encodeSpecialCharacters={true}
- prioritizeSeoTags={false}
- title="projects.page"
- />
- <withRouter(Search)
- component={
- {
- "breadcrumbs": [],
- "canBrowseAllChildProjects": true,
- "key": "foo",
- "name": "foo",
- "qualifier": "SVW",
- }
- }
- newCodeSelected={true}
- onNewCodeToggle={[Function]}
- onSearchClear={[Function]}
- onSearchResults={[Function]}
- />
- <div
- className="code-components"
- >
- <div
- className="boxed-group spacer-top"
- >
- <withKeyboardNavigation(Components)
- baseComponent={
- {
- "breadcrumbs": [],
- "canBrowseAllChildProjects": true,
- "key": "foo",
- "name": "foo",
- "qualifier": "SVW",
- }
- }
- components={
- [
- {
- "breadcrumbs": [],
- "key": "my-project",
- "name": "MyProject",
- "qualifier": "FIL",
- "qualityGate": {
- "isDefault": true,
- "key": "30",
- "name": "Sonar way",
- },
- "qualityProfiles": [
- {
- "deleted": false,
- "key": "my-qp",
- "language": "ts",
- "name": "Sonar way",
- },
- ],
- "tags": [],
- },
- ]
- }
- cycle={true}
- metrics={[]}
- newCodeSelected={true}
- onEndOfList={[Function]}
- onGoToParent={[Function]}
- onHighlight={[Function]}
- onSelect={[Function]}
- rootComponent={
- {
- "breadcrumbs": [],
- "canBrowseAllChildProjects": true,
- "key": "foo",
- "name": "foo",
- "qualifier": "SVW",
- }
- }
- />
- </div>
- <ListFooter
- count={1}
- loadMore={[Function]}
- total={1}
- />
- </div>
-</div>
-`;
-
-exports[`should render correclty when no sub component for TRK 1`] = `
-<div
- className="page page-limited"
->
- <A11ySkipTarget
- anchor="code_main"
- />
- <Suggestions
- suggestions="code"
- />
- <Helmet
- defer={false}
- encodeSpecialCharacters={true}
- prioritizeSeoTags={false}
- title="code.page"
- />
- <div
- className="code-components"
- >
- <div
- className="display-flex-center display-flex-column no-file"
- >
- <span
- className="h1 text-muted"
- >
- code_viewer.no_source_code_displayed_due_to_empty_analysis.TRK
- </span>
- </div>
- </div>
-</div>
-`;
-
-exports[`should render correclty when no sub component for TRK: no search 1`] = `
-<div
- className="page page-limited"
->
- <A11ySkipTarget
- anchor="code_main"
- />
- <Suggestions
- suggestions="code"
- />
- <Helmet
- defer={false}
- encodeSpecialCharacters={true}
- prioritizeSeoTags={false}
- title="code.page"
- />
- <withRouter(Search)
- component={
- {
- "breadcrumbs": [],
- "canBrowseAllChildProjects": true,
- "key": "foo",
- "name": "foo",
- "qualifier": "TRK",
- }
- }
- newCodeSelected={true}
- onNewCodeToggle={[Function]}
- onSearchClear={[Function]}
- onSearchResults={[Function]}
- />
- <div
- className="code-components"
- >
- <div
- className="boxed-group spacer-top search-results"
- >
- <withKeyboardNavigation(Components)
- components={[]}
- metrics={[]}
- onHighlight={[Function]}
- onSelect={[Function]}
- rootComponent={
- {
- "breadcrumbs": [],
- "canBrowseAllChildProjects": true,
- "key": "foo",
- "name": "foo",
- "qualifier": "TRK",
- }
- }
- />
- </div>
- </div>
-</div>
-`;
-
-exports[`should render correclty when no sub component for TRK: with sub component 1`] = `
-<div
- className="page page-limited"
->
- <A11ySkipTarget
- anchor="code_main"
- />
- <Suggestions
- suggestions="code"
- />
- <Helmet
- defer={false}
- encodeSpecialCharacters={true}
- prioritizeSeoTags={false}
- title="code.page"
- />
- <withRouter(Search)
- component={
- {
- "breadcrumbs": [],
- "canBrowseAllChildProjects": true,
- "key": "foo",
- "name": "foo",
- "qualifier": "TRK",
- }
- }
- newCodeSelected={true}
- onNewCodeToggle={[Function]}
- onSearchClear={[Function]}
- onSearchResults={[Function]}
- />
- <div
- className="code-components"
- >
- <div
- className="boxed-group spacer-top"
- >
- <withKeyboardNavigation(Components)
- baseComponent={
- {
- "breadcrumbs": [],
- "canBrowseAllChildProjects": true,
- "key": "foo",
- "name": "foo",
- "qualifier": "TRK",
- }
- }
- components={
- [
- {
- "breadcrumbs": [],
- "key": "my-project",
- "name": "MyProject",
- "qualifier": "FIL",
- "qualityGate": {
- "isDefault": true,
- "key": "30",
- "name": "Sonar way",
- },
- "qualityProfiles": [
- {
- "deleted": false,
- "key": "my-qp",
- "language": "ts",
- "name": "Sonar way",
- },
- ],
- "tags": [],
- },
- ]
- }
- cycle={true}
- metrics={
- [
- {
- "domain": "Coverage",
- "id": "2",
- "key": "coverage",
- "name": "Coverage",
- "type": "PERCENT",
- },
- ]
- }
- newCodeSelected={true}
- onEndOfList={[Function]}
- onGoToParent={[Function]}
- onHighlight={[Function]}
- onSelect={[Function]}
- rootComponent={
- {
- "breadcrumbs": [],
- "canBrowseAllChildProjects": true,
- "key": "foo",
- "name": "foo",
- "qualifier": "TRK",
- }
- }
- />
- </div>
- <ListFooter
- count={1}
- loadMore={[Function]}
- total={1}
- />
- </div>
-</div>
-`;
-
-exports[`should render correclty when no sub component for VW 1`] = `
-<div
- className="page page-limited"
->
- <A11ySkipTarget
- anchor="code_main"
- />
- <Suggestions
- suggestions="code"
- />
- <Helmet
- defer={false}
- encodeSpecialCharacters={true}
- prioritizeSeoTags={false}
- title="projects.page"
- />
- <div
- className="code-components"
- >
- <div
- className="display-flex-center display-flex-column no-file"
- >
- <span
- className="h1 text-muted"
- >
- code_viewer.no_source_code_displayed_due_to_empty_analysis.VW
- </span>
- </div>
- </div>
-</div>
-`;
-
-exports[`should render correclty when no sub component for VW: no search 1`] = `
-<div
- className="page page-limited"
->
- <A11ySkipTarget
- anchor="code_main"
- />
- <Suggestions
- suggestions="code"
- />
- <Helmet
- defer={false}
- encodeSpecialCharacters={true}
- prioritizeSeoTags={false}
- title="projects.page"
- />
- <withRouter(Search)
- component={
- {
- "breadcrumbs": [],
- "canBrowseAllChildProjects": true,
- "key": "foo",
- "name": "foo",
- "qualifier": "VW",
- }
- }
- newCodeSelected={true}
- onNewCodeToggle={[Function]}
- onSearchClear={[Function]}
- onSearchResults={[Function]}
- />
- <div
- className="code-components"
- >
- <div
- className="boxed-group spacer-top search-results"
- >
- <withKeyboardNavigation(Components)
- components={[]}
- metrics={[]}
- onHighlight={[Function]}
- onSelect={[Function]}
- rootComponent={
- {
- "breadcrumbs": [],
- "canBrowseAllChildProjects": true,
- "key": "foo",
- "name": "foo",
- "qualifier": "VW",
- }
- }
- />
- </div>
- </div>
-</div>
-`;
-
-exports[`should render correclty when no sub component for VW: with sub component 1`] = `
-<div
- className="page page-limited"
->
- <A11ySkipTarget
- anchor="code_main"
- />
- <Suggestions
- suggestions="code"
- />
- <Helmet
- defer={false}
- encodeSpecialCharacters={true}
- prioritizeSeoTags={false}
- title="projects.page"
- />
- <withRouter(Search)
- component={
- {
- "breadcrumbs": [],
- "canBrowseAllChildProjects": true,
- "key": "foo",
- "name": "foo",
- "qualifier": "VW",
- }
- }
- newCodeSelected={true}
- onNewCodeToggle={[Function]}
- onSearchClear={[Function]}
- onSearchResults={[Function]}
- />
- <div
- className="code-components"
- >
- <div
- className="boxed-group spacer-top"
- >
- <withKeyboardNavigation(Components)
- baseComponent={
- {
- "breadcrumbs": [],
- "canBrowseAllChildProjects": true,
- "key": "foo",
- "name": "foo",
- "qualifier": "VW",
- }
- }
- components={
- [
- {
- "breadcrumbs": [],
- "key": "my-project",
- "name": "MyProject",
- "qualifier": "FIL",
- "qualityGate": {
- "isDefault": true,
- "key": "30",
- "name": "Sonar way",
- },
- "qualityProfiles": [
- {
- "deleted": false,
- "key": "my-qp",
- "language": "ts",
- "name": "Sonar way",
- },
- ],
- "tags": [],
- },
- ]
- }
- cycle={true}
- metrics={[]}
- newCodeSelected={true}
- onEndOfList={[Function]}
- onGoToParent={[Function]}
- onHighlight={[Function]}
- onSelect={[Function]}
- rootComponent={
- {
- "breadcrumbs": [],
- "canBrowseAllChildProjects": true,
- "key": "foo",
- "name": "foo",
- "qualifier": "VW",
- }
- }
- />
- </div>
- <ListFooter
- count={1}
- loadMore={[Function]}
- total={1}
- />
- </div>
-</div>
-`;
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`] = `
-<Measure
- metricKey="coverage"
- metricType="PERCENT"
- value="1.0"
-/>
-`;
-
-exports[`renders correctly for leak values 1`] = `
-<Measure
- metricKey="new_coverage"
- metricType="PERCENT"
- value="1.0"
-/>
-`;
-
-exports[`renders correctly when component has no measures 1`] = `<span />`;
-
-exports[`should render correctly for releasability rating 1`] = `
-<Measure
- metricKey="alert_status"
- metricType="LEVEL"
- value="1.0"
-/>
-`;
-
-exports[`should render correctly when no measure matches the metric 1`] = `
-<span>
- —
-</span>
-`;
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`] = `
-<span
- aria-label="code.parent_folder"
- className="max-width-100 display-inline-block text-ellipsis"
- title="Foo
-
-foo"
->
- <span>
- <QualifierIcon
- fill="#333333"
- qualifier="APP"
- />
-
- Foo
- </span>
-</span>
-`;
-
-exports[`#ComponentName should render breadcrumb correctly for SVW 1`] = `
-<span
- aria-label="code.parent_folder"
- className="max-width-100 display-inline-block text-ellipsis"
- title="Foo
-
-foo"
->
- <span>
- <QualifierIcon
- fill="#333333"
- qualifier="SVW"
- />
-
- Foo
- </span>
-</span>
-`;
-
-exports[`#ComponentName should render breadcrumb correctly for TRK 1`] = `
-<span
- aria-label="code.parent_folder"
- className="max-width-100 display-inline-block text-ellipsis"
- title="Foo
-
-foo"
->
- <span>
- <QualifierIcon
- fill="#333333"
- qualifier="TRK"
- />
-
- Foo
- </span>
-</span>
-`;
-
-exports[`#ComponentName should render breadcrumb correctly for TRK 2`] = `
-<span
- aria-label="code.parent_folder"
- className="max-width-100 display-inline-block text-ellipsis"
- title="Foo
-
-foo"
->
- <span>
- <QualifierIcon
- fill="#333333"
- qualifier="TRK"
- />
-
- Foo
- </span>
-</span>
-`;
-
-exports[`#ComponentName should render breadcrumb correctly for VW 1`] = `
-<span
- aria-label="code.parent_folder"
- className="max-width-100 display-inline-block text-ellipsis"
- title="Foo
-
-foo"
->
- <span>
- <QualifierIcon
- fill="#333333"
- qualifier="VW"
- />
-
- Foo
- </span>
-</span>
-`;
-
-exports[`#ComponentName should render correctly for dirs 1`] = `
-<span
- className="max-width-100 display-inline-block text-ellipsis"
- title="src/main/ts/app
-
-foo"
->
- <span>
- <QualifierIcon
- fill="#ed7d20"
- qualifier="DIR"
- />
-
- <span>
- <span
- style={
- {
- "color": "#656565",
- }
- }
- >
- src/main/ts/
- </span>
- <span>
- app
- </span>
- </span>
- </span>
-</span>
-`;
-
-exports[`#ComponentName should render correctly for dirs 2`] = `
-<span
- className="max-width-100 display-inline-block text-ellipsis"
- title="src
-
-foo"
->
- <span>
- <QualifierIcon
- fill="#ed7d20"
- qualifier="DIR"
- />
-
- src
- </span>
-</span>
-`;
-
-exports[`#ComponentName should render correctly for files 1`] = `
-<span
- className="max-width-100 display-inline-block text-ellipsis"
- title="src/index.tsx
-
-foo:src/index.tsx"
->
- <span>
- <QualifierIcon
- fill="#333333"
- qualifier="FIL"
- />
-
- index.tsx
- </span>
-</span>
-`;
-
-exports[`#ComponentName should render correctly for files 2`] = `
-<span
- className="max-width-100 display-inline-block text-ellipsis"
- title="src/index.tsx
-
-foo:src/index.tsx"
->
- <ForwardRef(Link)
- className="display-inline-flex-center link-no-underline"
- to={
- {
- "pathname": "/code",
- "search": "?id=foo&selected=foo%3Asrc%2Findex.tsx",
- }
- }
- >
- <QualifierIcon
- className="little-spacer-right"
- fill="#236a97"
- qualifier="FIL"
- />
- <span>
- index.tsx
- </span>
- </ForwardRef(Link)>
-</span>
-`;
-
-exports[`#ComponentName should render correctly for files 3`] = `
-<span
- className="max-width-100 display-inline-block text-ellipsis"
- title="src/index.tsx
-
-foo:src/index.tsx"
->
- <span>
- <QualifierIcon
- fill="#333333"
- qualifier="FIL"
- />
-
- index.tsx
- </span>
-</span>
-`;
-
-exports[`#ComponentName should render correctly for files 4`] = `
-<span
- className="max-width-100 display-inline-block text-ellipsis"
- title="src/index.tsx
-
-foo:src/index.tsx"
->
- <span>
- <QualifierIcon
- fill="#333333"
- qualifier="FIL"
- />
-
- index.tsx
- </span>
-</span>
-`;
-
-exports[`#ComponentName should render correctly for files 5`] = `
-<span
- className="max-width-100 display-inline-block text-ellipsis"
- title="src/index.tsx
-
-foo:src/index.tsx"
->
- <span>
- <QualifierIcon
- fill="#333333"
- qualifier="FIL"
- />
-
- index.tsx
- </span>
-</span>
-`;
-
-exports[`#ComponentName should render correctly for files 6`] = `
-<span
- className="max-width-100 display-inline-block text-ellipsis"
- title="src/index.tsx
-
-foo:src/index.tsx"
->
- <span>
- <QualifierIcon
- fill="#333333"
- qualifier="FIL"
- />
-
- index.tsx
- </span>
-</span>
-`;
-
-exports[`#ComponentName should render correctly for files 7`] = `
-<span
- className="max-width-100 display-inline-block text-ellipsis"
- title="src/index.tsx
-
-foo:src/index.tsx"
->
- <span>
- <QualifierIcon
- fill="#333333"
- qualifier="FIL"
- />
-
- index.tsx
- </span>
-</span>
-`;
-
-exports[`#ComponentName should render correctly for refs 1`] = `
-<span
- className="max-width-100 display-inline-block text-ellipsis"
- title="Foo
-
-foo
-
-foo"
->
- <ForwardRef(Link)
- className="display-inline-flex-center link-no-underline"
- to={
- {
- "pathname": "/dashboard",
- "search": "?id=src%2Fmain%2Fts%2Fapp&code_scope=new",
- }
- }
- >
- <QualifierIcon
- className="little-spacer-right"
- fill="#236a97"
- qualifier="TRK"
- />
- <span>
- Foo
- </span>
- </ForwardRef(Link)>
-</span>
-`;
-
-exports[`#ComponentName should render correctly for refs 2`] = `
-<span
- className="max-width-100 display-inline-flex-center"
->
- <span
- className="text-ellipsis"
- title="Foo
-
-foo
-
-foo"
- >
- <ForwardRef(Link)
- className="display-inline-flex-center link-no-underline"
- to={
- {
- "pathname": "/dashboard",
- "search": "?id=src%2Fmain%2Fts%2Fapp&branch=foo&code_scope=new",
- }
- }
- >
- <QualifierIcon
- className="little-spacer-right"
- fill="#236a97"
- qualifier="TRK"
- />
- <span>
- Foo
- </span>
- </ForwardRef(Link)>
- </span>
- <span
- className="text-ellipsis spacer-left"
- >
- <BranchIcon
- className="little-spacer-right"
- />
- <span
- className="note"
- >
- foo
- </span>
- </span>
-</span>
-`;
-
-exports[`#ComponentName should render correctly for refs 3`] = `
-<span
- className="max-width-100 display-inline-flex-center"
->
- <span
- className="text-ellipsis"
- title="Foo
-
-foo"
- >
- <ForwardRef(Link)
- className="display-inline-flex-center link-no-underline"
- to={
- {
- "pathname": "/dashboard",
- "search": "?id=src%2Fmain%2Fts%2Fapp&code_scope=new",
- }
- }
- >
- <QualifierIcon
- className="little-spacer-right"
- fill="#236a97"
- qualifier="TRK"
- />
- <span>
- Foo
- </span>
- </ForwardRef(Link)>
- </span>
- <span
- className="spacer-left badge flex-1"
- >
- branches.main_branch
- </span>
-</span>
-`;
-
-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`] = `
-<table
- className="data boxed-padding zebra"
->
- <ComponentsHeader
- baseComponent={
- {
- "branch": "develop",
- "key": "foo",
- "name": "Foo",
- "qualifier": "TRK",
- }
- }
- canBePinned={true}
- metrics={
- [
- "coverage",
- ]
- }
- rootComponent={
- {
- "branch": "develop",
- "key": "foo",
- "name": "Foo",
- "qualifier": "TRK",
- }
- }
- />
- <tbody>
- <withScrollTo(Component)
- canBePinned={true}
- component={
- {
- "branch": "develop",
- "key": "foo",
- "name": "Foo",
- "qualifier": "TRK",
- }
- }
- hasBaseComponent={false}
- isBaseComponent={true}
- key="foo"
- metrics={
- [
- {
- "id": "1",
- "key": "coverage",
- "name": "Coverage",
- "type": "PERCENT",
- },
- ]
- }
- rootComponent={
- {
- "branch": "develop",
- "key": "foo",
- "name": "Foo",
- "qualifier": "TRK",
- }
- }
- />
- <tr
- className="blank"
- >
- <td
- colSpan={3}
- >
- <hr
- className="null-spacer-top"
- />
- </td>
- <td
- colSpan={5}
- >
- <hr
- className="null-spacer-top"
- />
- </td>
- </tr>
- <withScrollTo(Component)
- canBePinned={true}
- canBrowse={true}
- component={
- {
- "branch": "develop",
- "key": "foo",
- "name": "Foo",
- "qualifier": "TRK",
- }
- }
- hasBaseComponent={true}
- key="foo/develop"
- metrics={
- [
- {
- "id": "1",
- "key": "coverage",
- "name": "Coverage",
- "type": "PERCENT",
- },
- ]
- }
- rootComponent={
- {
- "branch": "develop",
- "key": "foo",
- "name": "Foo",
- "qualifier": "TRK",
- }
- }
- />
- <tr
- className="blank"
- >
- <td
- colSpan={3}
- />
- <td
- colSpan={5}
- />
- </tr>
- </tbody>
-</table>
-`;
-
-exports[`renders correctly for a search 1`] = `
-<table
- className="data boxed-padding zebra"
->
- <tbody>
- <withScrollTo(Component)
- canBrowse={true}
- component={
- {
- "branch": "develop",
- "key": "foo",
- "name": "Foo",
- "qualifier": "TRK",
- }
- }
- hasBaseComponent={false}
- key="foo/develop"
- metrics={[]}
- rootComponent={
- {
- "branch": "develop",
- "key": "foo",
- "name": "Foo",
- "qualifier": "TRK",
- }
- }
- />
- <tr
- className="blank"
- >
- <td
- colSpan={3}
- />
- <td
- colSpan={4}
- />
- </tr>
- </tbody>
-</table>
-`;
-
-exports[`renders correctly for leak 1`] = `
-<table
- className="data boxed-padding zebra"
->
- <ComponentsHeader
- baseComponent={
- {
- "branch": "develop",
- "key": "foo",
- "name": "Foo",
- "qualifier": "TRK",
- }
- }
- canBePinned={true}
- metrics={
- [
- "coverage",
- ]
- }
- rootComponent={
- {
- "branch": "develop",
- "key": "foo",
- "name": "Foo",
- "qualifier": "TRK",
- }
- }
- />
- <tbody>
- <withScrollTo(Component)
- branchLike={
- {
- "analysisDate": "2018-01-01",
- "excludedFromPurge": true,
- "isMain": false,
- "name": "feature",
- }
- }
- canBePinned={true}
- component={
- {
- "branch": "develop",
- "key": "foo",
- "name": "Foo",
- "qualifier": "TRK",
- }
- }
- hasBaseComponent={false}
- isBaseComponent={true}
- key="foo"
- metrics={
- [
- {
- "id": "1",
- "key": "coverage",
- "name": "Coverage",
- "type": "PERCENT",
- },
- ]
- }
- rootComponent={
- {
- "branch": "develop",
- "key": "foo",
- "name": "Foo",
- "qualifier": "TRK",
- }
- }
- />
- <tr
- className="blank"
- >
- <td
- colSpan={3}
- >
- <hr
- className="null-spacer-top"
- />
- </td>
- <td
- colSpan={5}
- >
- <hr
- className="null-spacer-top"
- />
- </td>
- </tr>
- <withScrollTo(Component)
- branchLike={
- {
- "analysisDate": "2018-01-01",
- "excludedFromPurge": true,
- "isMain": false,
- "name": "feature",
- }
- }
- canBePinned={true}
- canBrowse={true}
- component={
- {
- "branch": "develop",
- "key": "foo",
- "name": "Foo",
- "qualifier": "TRK",
- }
- }
- hasBaseComponent={true}
- key="foo/develop"
- metrics={
- [
- {
- "id": "1",
- "key": "coverage",
- "name": "Coverage",
- "type": "PERCENT",
- },
- ]
- }
- rootComponent={
- {
- "branch": "develop",
- "key": "foo",
- "name": "Foo",
- "qualifier": "TRK",
- }
- }
- />
- <tr
- className="blank"
- >
- <td
- colSpan={3}
- />
- <td
- colSpan={5}
- />
- </tr>
- </tbody>
-</table>
-`;
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`] = `
-<thead>
- <tr
- className="code-components-header"
- >
- <th
- className="thin nowrap"
- colSpan={2}
- />
- <th />
- <th />
- </tr>
-</thead>
-`;
-
-exports[`renders correctly for portfolios 1`] = `
-<thead>
- <tr
- className="code-components-header"
- >
- <th
- className="thin nowrap"
- colSpan={2}
- />
- <th />
- <th
- className="thin text-center"
- key="metric_domain.Releasability"
- >
- metric_domain.Releasability
- </th>
- <th
- className="thin text-center"
- key="metric_domain.Reliability"
- >
- metric_domain.Reliability
- </th>
- <th
- className="thin text-center"
- key="portfolio.metric_domain.vulnerabilities"
- >
- portfolio.metric_domain.vulnerabilities
- </th>
- <th
- className="thin text-center"
- key="portfolio.metric_domain.security_hotspots"
- >
- portfolio.metric_domain.security_hotspots
- </th>
- <th
- className="thin text-center"
- key="metric_domain.Maintainability"
- >
- metric_domain.Maintainability
- </th>
- <th
- className="thin text-right"
- key="metric.ncloc.name"
- >
- metric.ncloc.name
- </th>
- <th />
- </tr>
-</thead>
-`;
-
-exports[`renders correctly for projects 1`] = `
-<thead>
- <tr
- className="code-components-header"
- >
- <th
- className="thin nowrap"
- colSpan={2}
- />
- <th />
- <th
- className="thin nowrap text-right"
- key="metric.foo.name"
- >
- metric.foo.name
- </th>
- <th
- className="thin code-components-cell nowrap text-right"
- key="metric.bar.name"
- >
- metric.bar.name
- </th>
- <th />
- </tr>
-</thead>
-`;
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`] = `
-<div
- className="code-search"
- id="code-search"
->
- <SearchBox
- minLength={3}
- onChange={[Function]}
- onKeyDown={[Function]}
- placeholder="code.search_placeholder"
- value=""
- />
- <DeferredSpinner
- className="spacer-left"
- loading={false}
- />
-</div>
-`;
-
-exports[`should render correcly: new code toggle for portfolio 1`] = `
-<div
- className="code-search"
- id="code-search"
->
- <span
- className="big-spacer-right"
- >
- <ButtonToggle
- disabled={false}
- onCheck={[MockFunction]}
- options={
- [
- {
- "label": "projects.view.new_code",
- "value": true,
- },
- {
- "label": "projects.view.overall_code",
- "value": false,
- },
- ]
- }
- value={false}
- />
- </span>
- <SearchBox
- minLength={3}
- onChange={[Function]}
- onKeyDown={[Function]}
- placeholder="code.search_placeholder.portfolio"
- value=""
- />
- <DeferredSpinner
- className="spacer-left"
- loading={false}
- />
-</div>
-`;
-
-exports[`should render correcly: new code toggle for portfolio disabled 1`] = `
-<div
- className="code-search"
- id="code-search"
->
- <span
- className="big-spacer-right"
- >
- <ButtonToggle
- disabled={true}
- onCheck={[MockFunction]}
- options={
- [
- {
- "label": "projects.view.new_code",
- "value": true,
- },
- {
- "label": "projects.view.overall_code",
- "value": false,
- },
- ]
- }
- value={false}
- />
- </span>
- <SearchBox
- minLength={3}
- onChange={[Function]}
- onKeyDown={[Function]}
- placeholder="code.search_placeholder.portfolio"
- value="bar"
- />
- <DeferredSpinner
- className="spacer-left"
- loading={true}
- />
-</div>
-`;
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<SourceViewer['props']>) {
- return renderComponent(getSourceViewerUi(override));
-}
-
-function getSourceViewerUi(override?: Partial<SourceViewer['props']>) {
- return (
+ const { rerender } = renderComponent(
<SourceViewer
aroundLine={1}
branchLike={undefined}
- component={handler.getFileWithSource()}
+ component={componentsHandler.getNonEmptyFileKey()}
displayAllIssues={true}
displayIssueLocationsCount={true}
displayIssueLocationsLink={false}
@@ -417,4 +424,23 @@ function getSourceViewerUi(override?: Partial<SourceViewer['props']>) {
{...override}
/>
);
+ return function (reoverride?: Partial<SourceViewer['props']>) {
+ rerender(
+ <SourceViewer
+ aroundLine={1}
+ branchLike={undefined}
+ component={componentsHandler.getNonEmptyFileKey()}
+ displayAllIssues={true}
+ displayIssueLocationsCount={true}
+ displayIssueLocationsLink={false}
+ displayLocationMarkers={true}
+ onIssueChange={jest.fn()}
+ onIssueSelect={jest.fn()}
+ onLoaded={jest.fn()}
+ onLocationSelect={jest.fn()}
+ {...override}
+ {...reoverride}
+ />
+ );
+ };
}
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<P>(
- WrappedComponent: React.ComponentClass<P & Partial<WithKeyboardNavigationProps>>
+ WrappedComponent: React.ComponentType<P & Partial<WithKeyboardNavigationProps>>
) {
return class Wrapper extends React.Component<P & WithKeyboardNavigationProps> {
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> = {}): DuplicatedFile {
+ return {
+ key: 'file1.java',
+ name: overrides.key || 'file1.java',
+ project: 'foo',
+ projectName: 'Foo',
+ ...overrides,
+ };
+}
+
+export function mockDuplication(overrides: Partial<Duplication> = {}): Duplication {
+ return {
+ blocks: [mockDuplicationBlock()],
+ ...overrides,
+ };
+}
+
+export function mockDuplicationBlock(overrides: Partial<DuplicationBlock> = {}): DuplicationBlock {
+ return {
+ from: 12,
+ size: 5,
+ ...overrides,
+ };
+}