From 793625f284d5512842703dba6487520380defa07 Mon Sep 17 00:00:00 2001 From: 7PH Date: Fri, 14 Apr 2023 11:48:14 +0200 Subject: [PATCH] SONAR-18554 Migrate issues app cross component source viewer to RTL --- .../js/api/mocks/ComponentsServiceMock.ts | 2 +- .../main/js/api/mocks/IssuesServiceMock.ts | 20 +- .../js/apps/issues/__tests__/IssuesApp-it.tsx | 96 +- .../__tests__/IssuesSourceViewer-it.tsx | 121 +++ .../ComponentSourceSnippetGroupViewer.tsx | 8 +- .../SnippetViewer.tsx | 2 +- ...ComponentSourceSnippetGroupViewer-test.tsx | 336 ------ .../CrossComponentSourceViewer-test.tsx | 143 --- .../IssueSourceViewerHeader-test.tsx | 61 +- .../__tests__/SnippetViewer-test.tsx | 87 +- ...nentSourceSnippetGroupViewer-test.tsx.snap | 39 - .../CrossComponentSourceViewer-test.tsx.snap | 472 --------- .../IssueSourceViewerHeader-test.tsx.snap | 276 ----- .../__snapshots__/SnippetViewer-test.tsx.snap | 956 ------------------ .../src/main/js/apps/issues/test-utils.tsx | 105 ++ 15 files changed, 336 insertions(+), 2388 deletions(-) create mode 100644 server/sonar-web/src/main/js/apps/issues/__tests__/IssuesSourceViewer-it.tsx delete mode 100644 server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/ComponentSourceSnippetGroupViewer-test.tsx delete mode 100644 server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/CrossComponentSourceViewer-test.tsx delete mode 100644 server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/__snapshots__/ComponentSourceSnippetGroupViewer-test.tsx.snap delete mode 100644 server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/__snapshots__/CrossComponentSourceViewer-test.tsx.snap delete mode 100644 server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/__snapshots__/IssueSourceViewerHeader-test.tsx.snap delete mode 100644 server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/__snapshots__/SnippetViewer-test.tsx.snap create mode 100644 server/sonar-web/src/main/js/apps/issues/test-utils.tsx diff --git a/server/sonar-web/src/main/js/api/mocks/ComponentsServiceMock.ts b/server/sonar-web/src/main/js/api/mocks/ComponentsServiceMock.ts index 359292c3f7e..df199501842 100644 --- a/server/sonar-web/src/main/js/api/mocks/ComponentsServiceMock.ts +++ b/server/sonar-web/src/main/js/api/mocks/ComponentsServiceMock.ts @@ -42,6 +42,7 @@ import { SourceViewerFile, } from '../../types/types'; import { + GetTreeParams, changeKey, getChildren, getComponentData, @@ -50,7 +51,6 @@ import { getDuplications, getSources, getTree, - GetTreeParams, } from '../components'; interface ComponentTree { 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 1809ed0dbc1..22af6d71357 100644 --- a/server/sonar-web/src/main/js/api/mocks/IssuesServiceMock.ts +++ b/server/sonar-web/src/main/js/api/mocks/IssuesServiceMock.ts @@ -59,8 +59,8 @@ import { editIssueComment, getIssueChangelog, getIssueFlowSnippets, - searchIssues, searchIssueTags, + searchIssues, setIssueAssignee, setIssueSeverity, setIssueTags, @@ -282,7 +282,7 @@ export default class IssuesServiceMock { { issue: mockRawIssue(false, { key: 'issue1', - component: 'foo:test1.js', + component: 'foo:huge.js', message: 'Fix this', type: IssueType.Vulnerability, rule: 'simpleRuleId', @@ -296,7 +296,7 @@ export default class IssuesServiceMock { { locations: [ { - component: 'foo:test1.js', + component: 'foo:huge.js', msg: 'location 1', textRange: { startLine: 1, @@ -310,11 +310,11 @@ export default class IssuesServiceMock { { locations: [ { - component: 'foo:test2.js', + component: 'foo:huge.js', msg: 'location 2', textRange: { - startLine: 20, - endLine: 20, + startLine: 50, + endLine: 50, startOffset: 0, endOffset: 1, }, @@ -326,14 +326,14 @@ export default class IssuesServiceMock { snippets: keyBy( [ mockSnippetsByComponent( - 'test1.js', + 'huge.js', 'foo', - times(40, (i) => i + 1) + times(80, (i) => i + 1) ), mockSnippetsByComponent( - 'test2.js', + 'huge.js', 'foo', - times(40, (i) => i + 1) + times(80, (i) => i + 1) ), ], 'component.key' 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 fbe22d96bd0..b555adc42e3 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 @@ -17,32 +17,22 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import { act, screen, waitFor, within } from '@testing-library/react'; +import { act, screen, within } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; -import React from 'react'; import selectEvent from 'react-select-event'; -import { byLabelText, byRole } from 'testing-library-selector'; -import ComponentsServiceMock from '../../../api/mocks/ComponentsServiceMock'; -import IssuesServiceMock from '../../../api/mocks/IssuesServiceMock'; import { TabKeys } from '../../../components/rules/RuleTabViewer'; -import { mockComponent } from '../../../helpers/mocks/component'; import { renderOwaspTop102021Category } from '../../../helpers/security-standard'; -import { mockCurrentUser, mockLoggedInUser } from '../../../helpers/testMocks'; -import { renderApp, renderAppWithComponentContext } from '../../../helpers/testReactTestingUtils'; +import { mockLoggedInUser } from '../../../helpers/testMocks'; import { ComponentQualifier } from '../../../types/component'; import { IssueType } from '../../../types/issues'; -import { Component } from '../../../types/types'; -import { CurrentUser } from '../../../types/users'; -import IssuesApp from '../components/IssuesApp'; -import { projectIssuesRoutes } from '../routes'; - -jest.mock('../../../api/issues'); -jest.mock('../../../api/rules'); -jest.mock('../../../api/components'); -jest.mock('../../../api/users'); - -const issuesHandler = new IssuesServiceMock(); -const componentsHandler = new ComponentsServiceMock(); +import { + componentsHandler, + issuesHandler, + renderIssueApp, + renderProjectIssuesApp, + ui, + waitOnDataLoaded, +} from '../test-utils'; beforeEach(() => { issuesHandler.reset(); @@ -51,59 +41,6 @@ beforeEach(() => { window.HTMLElement.prototype.scrollTo = jest.fn(); }); -const ui = { - loading: byLabelText('loading'), - issueItems: byRole('region'), - - issueItem1: byRole('region', { name: 'Issue with no location message' }), - issueItem2: byRole('region', { name: 'FlowIssue' }), - issueItem3: byRole('region', { name: 'Issue on file' }), - issueItem4: byRole('region', { name: 'Fix this' }), - issueItem5: byRole('region', { name: 'Fix that' }), - issueItem6: byRole('region', { name: 'Second issue' }), - issueItem7: byRole('region', { name: 'Issue with tags' }), - issueItem8: byRole('region', { name: 'Issue on page 2' }), - - clearIssueTypeFacet: byRole('button', { name: 'clear_x_filter.issues.facet.types' }), - codeSmellIssueTypeFilter: byRole('checkbox', { name: 'issue.type.CODE_SMELL' }), - vulnerabilityIssueTypeFilter: byRole('checkbox', { name: 'issue.type.VULNERABILITY' }), - clearSeverityFacet: byRole('button', { name: 'clear_x_filter.issues.facet.severities' }), - majorSeverityFilter: byRole('checkbox', { name: 'severity.MAJOR' }), - scopeFacet: byRole('button', { name: 'issues.facet.scopes' }), - clearScopeFacet: byRole('button', { name: 'clear_x_filter.issues.facet.scopes' }), - mainScopeFilter: byRole('checkbox', { name: 'issue.scope.MAIN' }), - resolutionFacet: byRole('button', { name: 'issues.facet.resolutions' }), - clearResolutionFacet: byRole('button', { name: 'clear_x_filter.issues.facet.resolutions' }), - fixedResolutionFilter: byRole('checkbox', { name: 'issue.resolution.FIXED' }), - statusFacet: byRole('button', { name: 'issues.facet.statuses' }), - creationDateFacet: byRole('button', { name: 'issues.facet.createdAt' }), - clearCreationDateFacet: byRole('button', { name: 'clear_x_filter.issues.facet.createdAt' }), - clearStatusFacet: byRole('button', { name: 'clear_x_filter.issues.facet.statuses' }), - openStatusFilter: byRole('checkbox', { name: 'issue.status.OPEN' }), - confirmedStatusFilter: byRole('checkbox', { name: 'issue.status.CONFIRMED' }), - ruleFacet: byRole('button', { name: 'issues.facet.rules' }), - clearRuleFacet: byRole('button', { name: 'clear_x_filter.issues.facet.rules' }), - tagFacet: byRole('button', { name: 'issues.facet.tags' }), - clearTagFacet: byRole('button', { name: 'clear_x_filter.issues.facet.tags' }), - projectFacet: byRole('button', { name: 'issues.facet.projects' }), - clearProjectFacet: byRole('button', { name: 'clear_x_filter.issues.facet.projects' }), - assigneeFacet: byRole('button', { name: 'issues.facet.assignees' }), - clearAssigneeFacet: byRole('button', { name: 'clear_x_filter.issues.facet.assignees' }), - authorFacet: byRole('button', { name: 'issues.facet.authors' }), - clearAuthorFacet: byRole('button', { name: 'clear_x_filter.issues.facet.authors' }), - - dateInputMonthSelect: byRole('combobox', { name: 'Month:' }), - dateInputYearSelect: byRole('combobox', { name: 'Year:' }), - - clearAllFilters: byRole('button', { name: 'clear_all_filters' }), -}; - -async function waitOnDataLoaded() { - await waitFor(() => { - expect(ui.loading.query()).not.toBeInTheDocument(); - }); -} - describe('issues app', () => { describe('rendering', () => { it('should show warning when not all issues are accessible', async () => { @@ -924,16 +861,3 @@ describe('redirects', () => { ).toBeInTheDocument(); }); }); - -function renderIssueApp(currentUser?: CurrentUser) { - renderApp('project/issues', , { currentUser: mockCurrentUser(currentUser) }); -} - -function renderProjectIssuesApp(navigateTo?: string, overrides?: Partial) { - renderAppWithComponentContext( - 'project/issues', - projectIssuesRoutes, - { navigateTo }, - { component: mockComponent(overrides) } - ); -} diff --git a/server/sonar-web/src/main/js/apps/issues/__tests__/IssuesSourceViewer-it.tsx b/server/sonar-web/src/main/js/apps/issues/__tests__/IssuesSourceViewer-it.tsx new file mode 100644 index 00000000000..f177dae4f0a --- /dev/null +++ b/server/sonar-web/src/main/js/apps/issues/__tests__/IssuesSourceViewer-it.tsx @@ -0,0 +1,121 @@ +/* + * SonarQube + * Copyright (C) 2009-2023 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import { screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { byRole, byText } from 'testing-library-selector'; +import { + componentsHandler, + issuesHandler, + renderProjectIssuesApp, + waitOnDataLoaded, +} from '../test-utils'; + +beforeEach(() => { + issuesHandler.reset(); + componentsHandler.reset(); + window.scrollTo = jest.fn(); + window.HTMLElement.prototype.scrollTo = jest.fn(); +}); + +const ui = { + expandAllLines: byRole('button', { name: 'source_viewer.expand_all_lines' }), + expandLinesAbove: byRole('button', { name: 'source_viewer.expand_above' }), + expandLinesBelow: byRole('button', { name: 'source_viewer.expand_below' }), + + line1: byRole('button', { name: 'source_viewer.line_X.1' }), + line44: byRole('button', { name: 'source_viewer.line_X.44' }), + line45: byRole('button', { name: 'source_viewer.line_X.45' }), + line60: byRole('button', { name: 'source_viewer.line_X.60' }), + line199: byRole('button', { name: 'source_viewer.line_X.199' }), + + scmInfoLine180: byRole('button', { + name: 'source_viewer.author_X.simon.brandhof@sonarsource.com, source_viewer.click_for_scm_info.1', + expanded: false, + }), + scmInfoExpanded: byText('80f564becc0c0a1c9abaa006eca83a4fd278c3f0'), +}; + +describe('issues source viewer', () => { + it('should show source across components', async () => { + const user = userEvent.setup(); + renderProjectIssuesApp('project/issues?issues=issue101&open=issue101&id=myproject'); + await waitOnDataLoaded(); + + expect(screen.getByRole('separator', { name: 'test1.js' })).toBeInTheDocument(); + expect(screen.getByRole('separator', { name: 'test2.js' })).toBeInTheDocument(); + + // Both line 1 of test1.js and test2.js should be rendered after expanding lines above snippet in test2.js + expect(ui.line1.getAll()).toHaveLength(1); + await user.click(ui.expandLinesAbove.get()); + expect(ui.line1.getAll()).toHaveLength(2); + }); + + it('should expand all lines and show SCM info', async () => { + const user = userEvent.setup(); + renderProjectIssuesApp('project/issues?issues=issue1&open=issue1&id=myproject'); + await waitOnDataLoaded(); + + expect(ui.line44.query()).not.toBeInTheDocument(); + expect(ui.line45.get()).toBeInTheDocument(); + expect(ui.line199.query()).not.toBeInTheDocument(); + + // Expand lines below snippet + const expandBelowSecondSnippet = ui.expandLinesBelow.getAll()[1]; + await user.click(expandBelowSecondSnippet); + expect(ui.line60.get()).toBeInTheDocument(); + expect(ui.line199.query()).not.toBeInTheDocument(); // Expand should only expand a few lines, not all of them + + // Expand all lines from issues source header + await user.click(ui.expandAllLines.get()); + + // all lines should be rendered now + expect(ui.line1.get()).toBeInTheDocument(); + expect(ui.line44.get()).toBeInTheDocument(); + expect(ui.line45.get()).toBeInTheDocument(); + expect(ui.line199.get()).toBeInTheDocument(); + + // Show SCM info for newly expanded line + expect(ui.scmInfoExpanded.query()).not.toBeInTheDocument(); + await user.click(ui.scmInfoLine180.get()); + expect(ui.scmInfoExpanded.get()).toBeInTheDocument(); + }); + + it('should merge snippet viewers when expanding one near another', async () => { + const user = userEvent.setup(); + renderProjectIssuesApp('project/issues?issues=issue1&open=issue1&id=myproject'); + await waitOnDataLoaded(); + + // Line 44 is between both snippets, it should not be shown + expect(ui.line44.query()).not.toBeInTheDocument(); + + // There currently are two snippet shown + expect(screen.getAllByRole('table')).toHaveLength(2); + + // Expand lines above second snippet + await user.click(ui.expandLinesAbove.get()); + + // Line 44 should now be shown + expect(ui.line44.get()).toBeInTheDocument(); + + // Snippets should be automatically merged + // eslint-disable-next-line jest-dom/prefer-in-document + expect(screen.getAllByRole('table')).toHaveLength(1); + }); +}); diff --git a/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/ComponentSourceSnippetGroupViewer.tsx b/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/ComponentSourceSnippetGroupViewer.tsx index ea109c4ff51..194cdc3f7d1 100644 --- a/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/ComponentSourceSnippetGroupViewer.tsx +++ b/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/ComponentSourceSnippetGroupViewer.tsx @@ -20,9 +20,9 @@ import * as React from 'react'; import { FormattedMessage } from 'react-intl'; import { getSources } from '../../../api/components'; -import IssueMessageBox from '../../../components/issue/IssueMessageBox'; import getCoverageStatus from '../../../components/SourceViewer/helpers/getCoverageStatus'; import { locationsByLine } from '../../../components/SourceViewer/helpers/indexing'; +import IssueMessageBox from '../../../components/issue/IssueMessageBox'; import { Alert } from '../../../components/ui/Alert'; import { getBranchLikeQuery } from '../../../helpers/branch-like'; import { translate } from '../../../helpers/l10n'; @@ -34,23 +34,23 @@ import { Duplication, ExpandDirection, FlowLocation, - Issue as TypeIssue, IssuesByLine, Snippet, SnippetGroup, SourceLine, SourceViewerFile, + Issue as TypeIssue, } from '../../../types/types'; import { IssueSourceViewerScrollContext } from '../components/IssueSourceViewerScrollContext'; import IssueSourceViewerHeader from './IssueSourceViewerHeader'; import SnippetViewer from './SnippetViewer'; import { + EXPAND_BY_LINES, + MERGE_DISTANCE, createSnippets, expandSnippet, - EXPAND_BY_LINES, getPrimaryLocation, linesForSnippets, - MERGE_DISTANCE, } from './utils'; interface Props { diff --git a/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/SnippetViewer.tsx b/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/SnippetViewer.tsx index 9c3d35c92d2..2180aa4bf6a 100644 --- a/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/SnippetViewer.tsx +++ b/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/SnippetViewer.tsx @@ -19,7 +19,6 @@ */ import classNames from 'classnames'; import * as React from 'react'; -import ExpandSnippetIcon from '../../../components/icons/ExpandSnippetIcon'; import Line from '../../../components/SourceViewer/components/Line'; import { symbolsByLine } from '../../../components/SourceViewer/helpers/indexing'; import { getSecondaryIssueLocationsForLine } from '../../../components/SourceViewer/helpers/issueLocations'; @@ -27,6 +26,7 @@ import { optimizeHighlightedSymbols, optimizeLocationMessage, } from '../../../components/SourceViewer/helpers/lines'; +import ExpandSnippetIcon from '../../../components/icons/ExpandSnippetIcon'; import { translate } from '../../../helpers/l10n'; import { Duplication, diff --git a/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/ComponentSourceSnippetGroupViewer-test.tsx b/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/ComponentSourceSnippetGroupViewer-test.tsx deleted file mode 100644 index a8ad63029d5..00000000000 --- a/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/ComponentSourceSnippetGroupViewer-test.tsx +++ /dev/null @@ -1,336 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2023 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 { range, times } from 'lodash'; -import * as React from 'react'; -import { FormattedMessage } from 'react-intl'; -import { getSources } from '../../../../api/components'; -import IssueMessageBox from '../../../../components/issue/IssueMessageBox'; -import { mockBranch, mockMainBranch } from '../../../../helpers/mocks/branch-like'; -import { - mockSnippetsByComponent, - mockSourceLine, - mockSourceViewerFile, -} from '../../../../helpers/mocks/sources'; -import { mockFlowLocation, mockIssue } from '../../../../helpers/testMocks'; -import { waitAndUpdate } from '../../../../helpers/testUtils'; -import { ComponentQualifier } from '../../../../types/component'; -import { IssueStatus } from '../../../../types/issues'; -import { SnippetGroup } from '../../../../types/types'; -import ComponentSourceSnippetGroupViewer from '../ComponentSourceSnippetGroupViewer'; -import SnippetViewer from '../SnippetViewer'; - -jest.mock('../../../../api/components', () => ({ - getSources: jest.fn().mockResolvedValue([]), -})); - -beforeEach(() => { - jest.clearAllMocks(); -}); - -it('should render correctly', () => { - expect(shallowRender()).toMatchSnapshot(); -}); - -it('should render correctly with secondary locations', () => { - // issue with secondary locations but no flows - const issue = mockIssue(true, { - component: 'project:main.js', - flows: [], - textRange: { startLine: 7, endLine: 7, startOffset: 5, endOffset: 10 }, - }); - - const snippetGroup: SnippetGroup = { - locations: [ - mockFlowLocation({ - component: issue.component, - textRange: { startLine: 34, endLine: 34, startOffset: 0, endOffset: 0 }, - }), - mockFlowLocation({ - component: issue.component, - textRange: { startLine: 74, endLine: 74, startOffset: 0, endOffset: 0 }, - }), - ], - ...mockSnippetsByComponent('main.js', 'project', [ - ...range(2, 17), - ...range(29, 39), - ...range(69, 79), - ]), - }; - const wrapper = shallowRender({ issue, snippetGroup }); - expect(wrapper.state('snippets')).toHaveLength(3); - expect(wrapper.state('snippets')[0]).toEqual({ index: 0, start: 2, end: 16 }); - expect(wrapper.state('snippets')[1]).toEqual({ index: 2, start: 29, end: 39 }); - expect(wrapper.state('snippets')[2]).toEqual({ index: 3, start: 69, end: 79 }); -}); - -it('should render correctly with flows', () => { - // issue with flows but no secondary locations - const issue = mockIssue(true, { - component: 'project:main.js', - secondaryLocations: [], - textRange: { startLine: 7, endLine: 7, startOffset: 5, endOffset: 10 }, - }); - - const snippetGroup: SnippetGroup = { - locations: [ - mockFlowLocation({ - component: issue.component, - textRange: { startLine: 34, endLine: 34, startOffset: 0, endOffset: 0 }, - }), - mockFlowLocation({ - component: issue.component, - textRange: { startLine: 74, endLine: 74, startOffset: 0, endOffset: 0 }, - }), - ], - ...mockSnippetsByComponent('main.js', 'project', [ - ...range(2, 17), - ...range(29, 39), - ...range(69, 79), - ]), - }; - const wrapper = shallowRender({ issue, snippetGroup }); - expect(wrapper.state('snippets')).toHaveLength(3); - expect(wrapper.state('snippets')[0]).toEqual({ index: 0, start: 2, end: 16 }); - expect(wrapper.state('snippets')[1]).toEqual({ index: 1, start: 29, end: 39 }); - expect(wrapper.state('snippets')[2]).toEqual({ index: 2, start: 69, end: 79 }); - - // Check that locationsByLine is defined when isLastOccurenceOfPrimaryComponent - expect(wrapper.find(SnippetViewer).at(0).props().locationsByLine).not.toEqual({}); - - // If not, it should be an empty object: - const snippets = shallowRender({ - isLastOccurenceOfPrimaryComponent: false, - issue, - snippetGroup, - }).find(SnippetViewer); - - expect(snippets.at(0).props().locationsByLine).toEqual({}); - expect(snippets.at(1).props().locationsByLine).toEqual({}); -}); - -it('should render file-level issue correctly', () => { - // issue with secondary locations and no primary location - const issue = mockIssue(true, { - component: 'project:main.js', - flows: [], - textRange: undefined, - }); - - const wrapper = shallowRender({ - issue, - snippetGroup: { - locations: [ - mockFlowLocation({ - component: issue.component, - textRange: { startLine: 34, endLine: 34, startOffset: 0, endOffset: 0 }, - }), - ], - ...mockSnippetsByComponent('main.js', 'project', range(29, 39)), - }, - }); - - expect(wrapper.find('ContextConsumer').dive().find(IssueMessageBox).exists()).toBe(true); -}); - -it.each([ - ['file-level', ComponentQualifier.File, true, 'issue.closed.file_level'], - ['file-level', ComponentQualifier.File, false, 'issue.closed.project_level'], - ['project-level', ComponentQualifier.Project, false, 'issue.closed.project_level'], -])( - 'should render a closed %s issue correctly', - async (_level, componentQualifier, componentEnabled, expectedLabel) => { - // issue with secondary locations and no primary location - const issue = mockIssue(true, { - component: 'project:main.js', - componentQualifier, - componentEnabled, - flows: [], - textRange: undefined, - status: IssueStatus.Closed, - }); - - const wrapper = shallowRender({ - issue, - snippetGroup: { - locations: [], - ...mockSnippetsByComponent('main.js', 'project', range(1, 10)), - }, - }); - - await waitAndUpdate(wrapper); - - expect(wrapper.find(FormattedMessage).prop('id')).toEqual(expectedLabel); - expect(wrapper.find('ContextConsumer').exists()).toBe(false); - } -); - -it('should expand block', async () => { - (getSources as jest.Mock).mockResolvedValueOnce( - Object.values(mockSnippetsByComponent('a', 'project', range(6, 59)).sources) - ); - const issue = mockIssue(true, { - textRange: { startLine: 74, endLine: 74, startOffset: 5, endOffset: 10 }, - }); - const snippetGroup: SnippetGroup = { - locations: [ - mockFlowLocation({ - component: 'a', - textRange: { startLine: 74, endLine: 74, startOffset: 0, endOffset: 0 }, - }), - mockFlowLocation({ - component: 'a', - textRange: { startLine: 107, endLine: 107, startOffset: 0, endOffset: 0 }, - }), - ], - ...mockSnippetsByComponent('a', 'project', [...range(69, 83), ...range(102, 112)]), - }; - - const wrapper = shallowRender({ issue, snippetGroup }); - - wrapper.instance().expandBlock(0, 'up'); - await waitAndUpdate(wrapper); - - expect(getSources).toHaveBeenCalledWith({ from: 9, key: 'project:a', to: 68 }); - expect(wrapper.state('snippets')).toHaveLength(2); - expect(wrapper.state('snippets')[0]).toEqual({ index: 0, start: 19, end: 83 }); - expect(Object.keys(wrapper.state('additionalLines'))).toHaveLength(53); -}); - -it('should expand full component', async () => { - (getSources as jest.Mock).mockResolvedValueOnce( - Object.values(mockSnippetsByComponent('a', 'project', times(14)).sources) - ); - const snippetGroup: SnippetGroup = { - locations: [ - mockFlowLocation({ - component: 'a', - textRange: { startLine: 3, endLine: 3, startOffset: 0, endOffset: 0 }, - }), - mockFlowLocation({ - component: 'a', - textRange: { startLine: 12, endLine: 12, startOffset: 0, endOffset: 0 }, - }), - ], - ...mockSnippetsByComponent('a', 'project', [1, 2, 3, 4, 5, 10, 11, 12, 13, 14]), - }; - - const wrapper = shallowRender({ snippetGroup }); - - wrapper.instance().expandComponent(); - await waitAndUpdate(wrapper); - - expect(getSources).toHaveBeenCalledWith({ key: 'project:a' }); - expect(wrapper.state('snippets')).toHaveLength(1); - expect(wrapper.state('snippets')[0]).toEqual({ index: -1, start: 0, end: 13 }); -}); - -it('should get the right branch when expanding', async () => { - (getSources as jest.Mock).mockResolvedValueOnce( - Object.values(mockSnippetsByComponent('a', 'project', range(8, 67)).sources) - ); - const snippetGroup: SnippetGroup = { - locations: [mockFlowLocation()], - ...mockSnippetsByComponent('a', 'project', [1, 2, 3, 4, 5, 6, 7]), - }; - - const wrapper = shallowRender({ - branchLike: mockBranch({ name: 'asdf' }), - snippetGroup, - }); - - wrapper.instance().expandBlock(0, 'down'); - await waitAndUpdate(wrapper); - - expect(getSources).toHaveBeenCalledWith({ branch: 'asdf', from: 8, key: 'project:a', to: 67 }); -}); - -it('should handle symbol highlighting', () => { - const wrapper = shallowRender(); - expect(wrapper.state('highlightedSymbols')).toEqual([]); - wrapper.instance().handleSymbolClick(['foo']); - expect(wrapper.state('highlightedSymbols')).toEqual(['foo']); - wrapper.instance().handleSymbolClick(['foo']); - expect(wrapper.state('highlightedSymbols')).toEqual([]); -}); - -it('should correctly handle lines actions', () => { - const snippetGroup: SnippetGroup = { - locations: [ - mockFlowLocation({ - component: 'my-project:foo/bar.ts', - textRange: { startLine: 34, endLine: 34, startOffset: 0, endOffset: 0 }, - }), - mockFlowLocation({ - component: 'my-project:foo/bar.ts', - textRange: { startLine: 54, endLine: 54, startOffset: 0, endOffset: 0 }, - }), - ], - ...mockSnippetsByComponent( - 'foo/bar.ts', - 'my-project', - [32, 33, 34, 35, 36, 52, 53, 54, 55, 56] - ), - }; - const loadDuplications = jest.fn(); - const renderDuplicationPopup = jest.fn(); - - const wrapper = shallowRender({ - loadDuplications, - renderDuplicationPopup, - snippetGroup, - }); - - const line = mockSourceLine(); - wrapper.find('SnippetViewer').first().prop('loadDuplications')(line); - expect(loadDuplications).toHaveBeenCalledWith('my-project:foo/bar.ts', line); - - wrapper.find('SnippetViewer').first().prop('renderDuplicationPopup')(1, 13); - expect(renderDuplicationPopup).toHaveBeenCalledWith( - mockSourceViewerFile('foo/bar.ts', 'my-project'), - 1, - 13 - ); -}); - -function shallowRender(props: Partial = {}) { - const snippetGroup: SnippetGroup = { - component: mockSourceViewerFile(), - locations: [], - sources: [], - }; - return shallow( - - ); -} diff --git a/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/CrossComponentSourceViewer-test.tsx b/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/CrossComponentSourceViewer-test.tsx deleted file mode 100644 index 54d97642db0..00000000000 --- a/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/CrossComponentSourceViewer-test.tsx +++ /dev/null @@ -1,143 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2023 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 { getComponentForSourceViewer, getDuplications } from '../../../../api/components'; -import { getIssueFlowSnippets } from '../../../../api/issues'; -import { - mockSnippetsByComponent, - mockSourceLine, - mockSourceViewerFile, -} from '../../../../helpers/mocks/sources'; -import { mockFlowLocation, mockIssue } from '../../../../helpers/testMocks'; -import { waitAndUpdate } from '../../../../helpers/testUtils'; -import { IssueStatus } from '../../../../types/issues'; -import ComponentSourceSnippetGroupViewer from '../ComponentSourceSnippetGroupViewer'; -import CrossComponentSourceViewer from '../CrossComponentSourceViewer'; - -jest.mock('../../../../api/issues', () => { - const { mockSnippetsByComponent } = jest.requireActual('../../../../helpers/mocks/sources'); - return { - getIssueFlowSnippets: jest.fn().mockResolvedValue({ 'main.js': mockSnippetsByComponent() }), - }; -}); - -jest.mock('../../../../api/components', () => ({ - getDuplications: jest.fn().mockResolvedValue({}), - getComponentForSourceViewer: jest.fn().mockResolvedValue({}), -})); - -beforeEach(() => { - jest.clearAllMocks(); -}); - -it('should render correctly', async () => { - let wrapper = shallowRender(); - expect(wrapper).toMatchSnapshot(); - - await waitAndUpdate(wrapper); - expect(wrapper).toMatchSnapshot(); - - wrapper = shallowRender({ issue: mockIssue(true, { component: 'test.js', key: 'unknown' }) }); - await waitAndUpdate(wrapper); - - expect(wrapper).toMatchSnapshot('no component found'); -}); - -it('Should fetch data', async () => { - const wrapper = shallowRender(); - wrapper.instance().fetchIssueFlowSnippets(); - await waitAndUpdate(wrapper); - expect(getIssueFlowSnippets).toHaveBeenCalledWith('1'); - expect(wrapper.state('components')).toEqual( - expect.objectContaining({ 'main.js': mockSnippetsByComponent() }) - ); - - (getIssueFlowSnippets as jest.Mock).mockClear(); - wrapper.setProps({ issue: mockIssue(true, { key: 'foo' }) }); - expect(getIssueFlowSnippets).toHaveBeenCalledWith('foo'); -}); - -it.each([ - ['on a deleted file', false, { component: 'myproject' }], - ['', true, { component: 'main.js' }], -])('Should handle a closed issue %s', async (_, componentEnabled, expected) => { - const wrapper = shallowRender({ - issue: mockIssue(true, { componentEnabled, status: IssueStatus.Closed }), - }); - wrapper.instance().fetchIssueFlowSnippets(); - await waitAndUpdate(wrapper); - expect(getIssueFlowSnippets).not.toHaveBeenCalled(); - expect(getComponentForSourceViewer).toHaveBeenCalledWith(expect.objectContaining(expected)); -}); - -it('Should handle no access rights', async () => { - (getIssueFlowSnippets as jest.Mock).mockRejectedValueOnce({ status: 403 }); - - const wrapper = shallowRender(); - await waitAndUpdate(wrapper); - - expect(wrapper.state().notAccessible).toBe(true); - expect(wrapper).toMatchSnapshot(); -}); - -it('should handle duplication popup', async () => { - const files = { b: { key: 'b', name: 'B.tsx', project: 'foo', projectName: 'Foo' } }; - const duplications = [{ blocks: [{ _ref: '1', from: 1, size: 2 }] }]; - (getDuplications as jest.Mock).mockResolvedValueOnce({ duplications, files }); - - const wrapper = shallowRender(); - await waitAndUpdate(wrapper); - - wrapper.find(ComponentSourceSnippetGroupViewer).props().loadDuplications('foo', mockSourceLine()); - - await waitAndUpdate(wrapper); - expect(getDuplications).toHaveBeenCalledWith({ key: 'foo' }); - expect(wrapper.state('duplicatedFiles')).toEqual(files); - expect(wrapper.state('duplications')).toEqual(duplications); - expect(wrapper.state('duplicationsByLine')).toEqual({ '1': [0], '2': [0] }); - - expect( - wrapper - .find(ComponentSourceSnippetGroupViewer) - .props() - .renderDuplicationPopup(mockSourceViewerFile(), 0, 16) - ).toMatchSnapshot(); -}); - -function shallowRender(props: Partial = {}) { - return shallow( - - ); -} diff --git a/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/IssueSourceViewerHeader-test.tsx b/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/IssueSourceViewerHeader-test.tsx index 3f9934de683..d95876d1cde 100644 --- a/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/IssueSourceViewerHeader-test.tsx +++ b/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/IssueSourceViewerHeader-test.tsx @@ -17,28 +17,61 @@ * 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 { byRole, byText } from 'testing-library-selector'; import { mockMainBranch } from '../../../../helpers/mocks/branch-like'; import { mockSourceViewerFile } from '../../../../helpers/mocks/sources'; -import { ComponentQualifier } from '../../../../types/component'; +import { renderComponent } from '../../../../helpers/testReactTestingUtils'; import IssueSourceViewerHeader, { Props } from '../IssueSourceViewerHeader'; +const ui = { + expandAllLines: byRole('button', { name: 'source_viewer.expand_all_lines' }), + projectLink: byRole('link', { name: 'qualifier.TRK MyProject' }), + projectName: byText('MyProject'), + viewAllIssues: byRole('link', { name: 'source_viewer.view_all_issues' }), +}; + it('should render correctly', () => { - expect(shallowRender()).toMatchSnapshot(); - expect(shallowRender({ linkToProject: false })).toMatchSnapshot('no link to project'); - expect(shallowRender({ displayProjectName: false })).toMatchSnapshot('no project name'); - expect( - shallowRender({ - sourceViewerFile: mockSourceViewerFile('foo/bar.ts', 'my-project', { - q: ComponentQualifier.Project, - }), - }) - ).toMatchSnapshot('project root'); + renderIssueSourceViewerHeader(); + + expect(ui.expandAllLines.get()).toBeInTheDocument(); + expect(ui.projectLink.get()).toBeInTheDocument(); + expect(ui.projectName.get()).toBeInTheDocument(); + expect(ui.viewAllIssues.get()).toBeInTheDocument(); +}); + +it('should not render expandable link', () => { + renderIssueSourceViewerHeader({ expandable: false }); + + expect(ui.expandAllLines.query()).not.toBeInTheDocument(); +}); + +it('should not render link to project', () => { + renderIssueSourceViewerHeader({ linkToProject: false }); + + expect(ui.projectLink.query()).not.toBeInTheDocument(); + expect(ui.projectName.get()).toBeInTheDocument(); +}); + +it('should not render project name', () => { + renderIssueSourceViewerHeader({ displayProjectName: false }); + + expect(ui.projectLink.query()).not.toBeInTheDocument(); + expect(ui.projectName.query()).not.toBeInTheDocument(); +}); + +it('should render without issue expand all when no issue', () => { + renderIssueSourceViewerHeader({ + sourceViewerFile: mockSourceViewerFile('foo/bar.ts', 'my-project', { + measures: {}, + }), + }); + + expect(ui.viewAllIssues.query()).not.toBeInTheDocument(); }); -function shallowRender(props: Partial = {}) { - return shallow( +function renderIssueSourceViewerHeader(props: Partial = {}) { + return renderComponent( ({ @@ -32,84 +34,69 @@ beforeEach(() => { jest.clearAllMocks(); }); -it('should render correctly', () => { - const snippet = range(5, 8).map((line) => mockSourceLine({ line })); - const wrapper = shallowRender({ - snippet, - }); +const ui = { + expandAbove: byRole('button', { name: 'source_viewer.expand_above' }), + expandBelow: byRole('button', { name: 'source_viewer.expand_below' }), + scmInfo: byRole('button', { + name: 'source_viewer.author_X.simon.brandhof@sonarsource.com, source_viewer.click_for_scm_info.5', + }), +}; - expect(wrapper).toMatchSnapshot(); -}); - -it('should render correctly with no SCM', () => { +it('should render correctly', () => { const snippet = range(5, 8).map((line) => mockSourceLine({ line })); - const wrapper = shallowRender({ - displaySCM: false, + renderSnippetViewer({ snippet, }); - expect(wrapper).toMatchSnapshot(); -}); - -it('should render additional child in line', () => { - const sourceline = mockSourceLine({ line: 42 }); - - const child =
child
; - const renderAdditionalChildInLine = jest.fn().mockReturnValue(child); - const wrapper = shallowRender({ renderAdditionalChildInLine, snippet: [sourceline] }); - - wrapper.instance().renderLine({ - displayDuplications: false, - index: 1, - issueLocations: [], - line: sourceline, - snippet: [sourceline], - symbols: [], - verticalBuffer: 5, - }); - - expect(renderAdditionalChildInLine).toHaveBeenCalledWith(sourceline); + expect(ui.expandAbove.get()).toBeInTheDocument(); + expect(ui.expandBelow.get()).toBeInTheDocument(); + expect(ui.scmInfo.get()).toBeInTheDocument(); }); it('should render correctly when at the top of the file', () => { const snippet = range(1, 8).map((line) => mockSourceLine({ line })); - const wrapper = shallowRender({ + renderSnippetViewer({ snippet, }); - expect(wrapper).toMatchSnapshot(); + expect(ui.expandAbove.query()).not.toBeInTheDocument(); + expect(ui.expandBelow.get()).toBeInTheDocument(); }); it('should render correctly when at the bottom of the file', () => { const component = mockSourceViewerFile('foo/bar.ts', 'my-project', { measures: { lines: '14' } }); - const snippet = range(10, 14).map((line) => mockSourceLine({ line })); - const wrapper = shallowRender({ + const snippet = range(10, 15).map((line) => mockSourceLine({ line })); + renderSnippetViewer({ component, snippet, }); - expect(wrapper).toMatchSnapshot(); + expect(ui.expandAbove.get()).toBeInTheDocument(); + expect(ui.expandBelow.query()).not.toBeInTheDocument(); }); -it('should correctly handle expansion', () => { +it('should render correctly with no SCM', () => { const snippet = range(5, 8).map((line) => mockSourceLine({ line })); - const expandBlock = jest.fn(() => Promise.resolve()); - - const wrapper = shallowRender({ - expandBlock, - index: 2, + renderSnippetViewer({ + displaySCM: false, snippet, }); - wrapper.find('.expand-block-above button').first().simulate('click'); - expect(expandBlock).toHaveBeenCalledWith(2, 'up'); + expect(ui.scmInfo.query()).not.toBeInTheDocument(); +}); + +it('should render additional child in line', () => { + const sourceline = mockSourceLine({ line: 42 }); + + const child =
child
; + const renderAdditionalChildInLine = jest.fn().mockReturnValue(child); + renderSnippetViewer({ renderAdditionalChildInLine, snippet: [sourceline] }); - wrapper.find('.expand-block-below button').first().simulate('click'); - expect(expandBlock).toHaveBeenCalledWith(2, 'down'); + expect(screen.getByTestId('additional-child')).toBeInTheDocument(); }); -function shallowRender(props: Partial = {}) { - return shallow( +function renderSnippetViewer(props: Partial = {}) { + return renderComponent( - - -`; diff --git a/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/__snapshots__/CrossComponentSourceViewer-test.tsx.snap b/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/__snapshots__/CrossComponentSourceViewer-test.tsx.snap deleted file mode 100644 index 390838a9ebf..00000000000 --- a/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/__snapshots__/CrossComponentSourceViewer-test.tsx.snap +++ /dev/null @@ -1,472 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Should handle no access rights 1`] = ` - - code_viewer.no_source_code_displayed_due_to_security - -`; - -exports[`should handle duplication popup 1`] = ` - - [Function] - -`; - -exports[`should render correctly 1`] = ` -
- -
-`; - -exports[`should render correctly 2`] = ` - - - - - -`; - -exports[`should render correctly: no component found 1`] = ` - - - - - - - - -`; diff --git a/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/__snapshots__/IssueSourceViewerHeader-test.tsx.snap b/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/__snapshots__/IssueSourceViewerHeader-test.tsx.snap deleted file mode 100644 index c4028b22491..00000000000 --- a/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/__snapshots__/IssueSourceViewerHeader-test.tsx.snap +++ /dev/null @@ -1,276 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`should render correctly 1`] = ` -
-
- -
- - - - foo/ - - - bar.ts - -
-
- -
-
-
- - source_viewer.view_all_issues - -
- -
- - - -
-
-
-`; - -exports[`should render correctly: no link to project 1`] = ` -
-
-
- - - - MyProject - -
-
- - - - foo/ - - - bar.ts - -
-
- -
-
-
- - source_viewer.view_all_issues - -
- -
- - - -
-
-
-`; - -exports[`should render correctly: no project name 1`] = ` -
-
-
- - - - foo/ - - - bar.ts - -
-
- -
-
-
- - source_viewer.view_all_issues - -
- -
- - - -
-
-
-`; - -exports[`should render correctly: project root 1`] = ` -
- - -
- - - -
-
-
-`; diff --git a/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/__snapshots__/SnippetViewer-test.tsx.snap b/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/__snapshots__/SnippetViewer-test.tsx.snap deleted file mode 100644 index 9cd0ed9127e..00000000000 --- a/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/__snapshots__/SnippetViewer-test.tsx.snap +++ /dev/null @@ -1,956 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`should render correctly 1`] = ` -
-
-
- -
- - - import java.util.ArrayList;", - "coverageStatus": "covered", - "coveredConditions": 2, - "duplicated": false, - "isNew": true, - "line": 5, - "scmAuthor": "simon.brandhof@sonarsource.com", - "scmDate": "2018-12-11T10:48:39+0100", - "scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0", - } - } - loadDuplications={[MockFunction]} - onIssueSelect={[Function]} - onIssueUnselect={[Function]} - onIssuesClose={[Function]} - onIssuesOpen={[Function]} - onLocationSelect={[MockFunction]} - onSymbolClick={[MockFunction]} - openIssues={false} - renderDuplicationPopup={[MockFunction]} - secondaryIssueLocations={[]} - verticalBuffer={0} - /> - import java.util.ArrayList;", - "coverageStatus": "covered", - "coveredConditions": 2, - "duplicated": false, - "isNew": true, - "line": 6, - "scmAuthor": "simon.brandhof@sonarsource.com", - "scmDate": "2018-12-11T10:48:39+0100", - "scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0", - } - } - loadDuplications={[MockFunction]} - onIssueSelect={[Function]} - onIssueUnselect={[Function]} - onIssuesClose={[Function]} - onIssuesOpen={[Function]} - onLocationSelect={[MockFunction]} - onSymbolClick={[MockFunction]} - openIssues={false} - previousLine={ - { - "code": "import java.util.ArrayList;", - "coverageStatus": "covered", - "coveredConditions": 2, - "duplicated": false, - "isNew": true, - "line": 5, - "scmAuthor": "simon.brandhof@sonarsource.com", - "scmDate": "2018-12-11T10:48:39+0100", - "scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0", - } - } - renderDuplicationPopup={[MockFunction]} - secondaryIssueLocations={[]} - verticalBuffer={0} - /> - import java.util.ArrayList;", - "coverageStatus": "covered", - "coveredConditions": 2, - "duplicated": false, - "isNew": true, - "line": 7, - "scmAuthor": "simon.brandhof@sonarsource.com", - "scmDate": "2018-12-11T10:48:39+0100", - "scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0", - } - } - loadDuplications={[MockFunction]} - onIssueSelect={[Function]} - onIssueUnselect={[Function]} - onIssuesClose={[Function]} - onIssuesOpen={[Function]} - onLocationSelect={[MockFunction]} - onSymbolClick={[MockFunction]} - openIssues={false} - previousLine={ - { - "code": "import java.util.ArrayList;", - "coverageStatus": "covered", - "coveredConditions": 2, - "duplicated": false, - "isNew": true, - "line": 6, - "scmAuthor": "simon.brandhof@sonarsource.com", - "scmDate": "2018-12-11T10:48:39+0100", - "scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0", - } - } - renderDuplicationPopup={[MockFunction]} - secondaryIssueLocations={[]} - verticalBuffer={0} - /> - -
-
- -
-
-
-`; - -exports[`should render correctly when at the bottom of the file 1`] = ` -
-
-
- -
- - - import java.util.ArrayList;", - "coverageStatus": "covered", - "coveredConditions": 2, - "duplicated": false, - "isNew": true, - "line": 10, - "scmAuthor": "simon.brandhof@sonarsource.com", - "scmDate": "2018-12-11T10:48:39+0100", - "scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0", - } - } - loadDuplications={[MockFunction]} - onIssueSelect={[Function]} - onIssueUnselect={[Function]} - onIssuesClose={[Function]} - onIssuesOpen={[Function]} - onLocationSelect={[MockFunction]} - onSymbolClick={[MockFunction]} - openIssues={false} - renderDuplicationPopup={[MockFunction]} - secondaryIssueLocations={[]} - verticalBuffer={0} - /> - import java.util.ArrayList;", - "coverageStatus": "covered", - "coveredConditions": 2, - "duplicated": false, - "isNew": true, - "line": 11, - "scmAuthor": "simon.brandhof@sonarsource.com", - "scmDate": "2018-12-11T10:48:39+0100", - "scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0", - } - } - loadDuplications={[MockFunction]} - onIssueSelect={[Function]} - onIssueUnselect={[Function]} - onIssuesClose={[Function]} - onIssuesOpen={[Function]} - onLocationSelect={[MockFunction]} - onSymbolClick={[MockFunction]} - openIssues={false} - previousLine={ - { - "code": "import java.util.ArrayList;", - "coverageStatus": "covered", - "coveredConditions": 2, - "duplicated": false, - "isNew": true, - "line": 10, - "scmAuthor": "simon.brandhof@sonarsource.com", - "scmDate": "2018-12-11T10:48:39+0100", - "scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0", - } - } - renderDuplicationPopup={[MockFunction]} - secondaryIssueLocations={[]} - verticalBuffer={0} - /> - import java.util.ArrayList;", - "coverageStatus": "covered", - "coveredConditions": 2, - "duplicated": false, - "isNew": true, - "line": 12, - "scmAuthor": "simon.brandhof@sonarsource.com", - "scmDate": "2018-12-11T10:48:39+0100", - "scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0", - } - } - loadDuplications={[MockFunction]} - onIssueSelect={[Function]} - onIssueUnselect={[Function]} - onIssuesClose={[Function]} - onIssuesOpen={[Function]} - onLocationSelect={[MockFunction]} - onSymbolClick={[MockFunction]} - openIssues={false} - previousLine={ - { - "code": "import java.util.ArrayList;", - "coverageStatus": "covered", - "coveredConditions": 2, - "duplicated": false, - "isNew": true, - "line": 11, - "scmAuthor": "simon.brandhof@sonarsource.com", - "scmDate": "2018-12-11T10:48:39+0100", - "scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0", - } - } - renderDuplicationPopup={[MockFunction]} - secondaryIssueLocations={[]} - verticalBuffer={0} - /> - import java.util.ArrayList;", - "coverageStatus": "covered", - "coveredConditions": 2, - "duplicated": false, - "isNew": true, - "line": 13, - "scmAuthor": "simon.brandhof@sonarsource.com", - "scmDate": "2018-12-11T10:48:39+0100", - "scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0", - } - } - loadDuplications={[MockFunction]} - onIssueSelect={[Function]} - onIssueUnselect={[Function]} - onIssuesClose={[Function]} - onIssuesOpen={[Function]} - onLocationSelect={[MockFunction]} - onSymbolClick={[MockFunction]} - openIssues={false} - previousLine={ - { - "code": "import java.util.ArrayList;", - "coverageStatus": "covered", - "coveredConditions": 2, - "duplicated": false, - "isNew": true, - "line": 12, - "scmAuthor": "simon.brandhof@sonarsource.com", - "scmDate": "2018-12-11T10:48:39+0100", - "scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0", - } - } - renderDuplicationPopup={[MockFunction]} - secondaryIssueLocations={[]} - verticalBuffer={0} - /> - -
-
- -
-
-
-`; - -exports[`should render correctly when at the top of the file 1`] = ` -
-
- - - import java.util.ArrayList;", - "coverageStatus": "covered", - "coveredConditions": 2, - "duplicated": false, - "isNew": true, - "line": 1, - "scmAuthor": "simon.brandhof@sonarsource.com", - "scmDate": "2018-12-11T10:48:39+0100", - "scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0", - } - } - loadDuplications={[MockFunction]} - onIssueSelect={[Function]} - onIssueUnselect={[Function]} - onIssuesClose={[Function]} - onIssuesOpen={[Function]} - onLocationSelect={[MockFunction]} - onSymbolClick={[MockFunction]} - openIssues={false} - renderDuplicationPopup={[MockFunction]} - secondaryIssueLocations={[]} - verticalBuffer={0} - /> - import java.util.ArrayList;", - "coverageStatus": "covered", - "coveredConditions": 2, - "duplicated": false, - "isNew": true, - "line": 2, - "scmAuthor": "simon.brandhof@sonarsource.com", - "scmDate": "2018-12-11T10:48:39+0100", - "scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0", - } - } - loadDuplications={[MockFunction]} - onIssueSelect={[Function]} - onIssueUnselect={[Function]} - onIssuesClose={[Function]} - onIssuesOpen={[Function]} - onLocationSelect={[MockFunction]} - onSymbolClick={[MockFunction]} - openIssues={false} - previousLine={ - { - "code": "import java.util.ArrayList;", - "coverageStatus": "covered", - "coveredConditions": 2, - "duplicated": false, - "isNew": true, - "line": 1, - "scmAuthor": "simon.brandhof@sonarsource.com", - "scmDate": "2018-12-11T10:48:39+0100", - "scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0", - } - } - renderDuplicationPopup={[MockFunction]} - secondaryIssueLocations={[]} - verticalBuffer={0} - /> - import java.util.ArrayList;", - "coverageStatus": "covered", - "coveredConditions": 2, - "duplicated": false, - "isNew": true, - "line": 3, - "scmAuthor": "simon.brandhof@sonarsource.com", - "scmDate": "2018-12-11T10:48:39+0100", - "scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0", - } - } - loadDuplications={[MockFunction]} - onIssueSelect={[Function]} - onIssueUnselect={[Function]} - onIssuesClose={[Function]} - onIssuesOpen={[Function]} - onLocationSelect={[MockFunction]} - onSymbolClick={[MockFunction]} - openIssues={false} - previousLine={ - { - "code": "import java.util.ArrayList;", - "coverageStatus": "covered", - "coveredConditions": 2, - "duplicated": false, - "isNew": true, - "line": 2, - "scmAuthor": "simon.brandhof@sonarsource.com", - "scmDate": "2018-12-11T10:48:39+0100", - "scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0", - } - } - renderDuplicationPopup={[MockFunction]} - secondaryIssueLocations={[]} - verticalBuffer={0} - /> - import java.util.ArrayList;", - "coverageStatus": "covered", - "coveredConditions": 2, - "duplicated": false, - "isNew": true, - "line": 4, - "scmAuthor": "simon.brandhof@sonarsource.com", - "scmDate": "2018-12-11T10:48:39+0100", - "scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0", - } - } - loadDuplications={[MockFunction]} - onIssueSelect={[Function]} - onIssueUnselect={[Function]} - onIssuesClose={[Function]} - onIssuesOpen={[Function]} - onLocationSelect={[MockFunction]} - onSymbolClick={[MockFunction]} - openIssues={false} - previousLine={ - { - "code": "import java.util.ArrayList;", - "coverageStatus": "covered", - "coveredConditions": 2, - "duplicated": false, - "isNew": true, - "line": 3, - "scmAuthor": "simon.brandhof@sonarsource.com", - "scmDate": "2018-12-11T10:48:39+0100", - "scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0", - } - } - renderDuplicationPopup={[MockFunction]} - secondaryIssueLocations={[]} - verticalBuffer={0} - /> - import java.util.ArrayList;", - "coverageStatus": "covered", - "coveredConditions": 2, - "duplicated": false, - "isNew": true, - "line": 5, - "scmAuthor": "simon.brandhof@sonarsource.com", - "scmDate": "2018-12-11T10:48:39+0100", - "scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0", - } - } - loadDuplications={[MockFunction]} - onIssueSelect={[Function]} - onIssueUnselect={[Function]} - onIssuesClose={[Function]} - onIssuesOpen={[Function]} - onLocationSelect={[MockFunction]} - onSymbolClick={[MockFunction]} - openIssues={false} - previousLine={ - { - "code": "import java.util.ArrayList;", - "coverageStatus": "covered", - "coveredConditions": 2, - "duplicated": false, - "isNew": true, - "line": 4, - "scmAuthor": "simon.brandhof@sonarsource.com", - "scmDate": "2018-12-11T10:48:39+0100", - "scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0", - } - } - renderDuplicationPopup={[MockFunction]} - secondaryIssueLocations={[]} - verticalBuffer={0} - /> - import java.util.ArrayList;", - "coverageStatus": "covered", - "coveredConditions": 2, - "duplicated": false, - "isNew": true, - "line": 6, - "scmAuthor": "simon.brandhof@sonarsource.com", - "scmDate": "2018-12-11T10:48:39+0100", - "scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0", - } - } - loadDuplications={[MockFunction]} - onIssueSelect={[Function]} - onIssueUnselect={[Function]} - onIssuesClose={[Function]} - onIssuesOpen={[Function]} - onLocationSelect={[MockFunction]} - onSymbolClick={[MockFunction]} - openIssues={false} - previousLine={ - { - "code": "import java.util.ArrayList;", - "coverageStatus": "covered", - "coveredConditions": 2, - "duplicated": false, - "isNew": true, - "line": 5, - "scmAuthor": "simon.brandhof@sonarsource.com", - "scmDate": "2018-12-11T10:48:39+0100", - "scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0", - } - } - renderDuplicationPopup={[MockFunction]} - secondaryIssueLocations={[]} - verticalBuffer={0} - /> - import java.util.ArrayList;", - "coverageStatus": "covered", - "coveredConditions": 2, - "duplicated": false, - "isNew": true, - "line": 7, - "scmAuthor": "simon.brandhof@sonarsource.com", - "scmDate": "2018-12-11T10:48:39+0100", - "scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0", - } - } - loadDuplications={[MockFunction]} - onIssueSelect={[Function]} - onIssueUnselect={[Function]} - onIssuesClose={[Function]} - onIssuesOpen={[Function]} - onLocationSelect={[MockFunction]} - onSymbolClick={[MockFunction]} - openIssues={false} - previousLine={ - { - "code": "import java.util.ArrayList;", - "coverageStatus": "covered", - "coveredConditions": 2, - "duplicated": false, - "isNew": true, - "line": 6, - "scmAuthor": "simon.brandhof@sonarsource.com", - "scmDate": "2018-12-11T10:48:39+0100", - "scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0", - } - } - renderDuplicationPopup={[MockFunction]} - secondaryIssueLocations={[]} - verticalBuffer={0} - /> - -
-
- -
-
-
-`; - -exports[`should render correctly with no SCM 1`] = ` -
-
-
- -
- - - import java.util.ArrayList;", - "coverageStatus": "covered", - "coveredConditions": 2, - "duplicated": false, - "isNew": true, - "line": 5, - "scmAuthor": "simon.brandhof@sonarsource.com", - "scmDate": "2018-12-11T10:48:39+0100", - "scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0", - } - } - loadDuplications={[MockFunction]} - onIssueSelect={[Function]} - onIssueUnselect={[Function]} - onIssuesClose={[Function]} - onIssuesOpen={[Function]} - onLocationSelect={[MockFunction]} - onSymbolClick={[MockFunction]} - openIssues={false} - renderDuplicationPopup={[MockFunction]} - secondaryIssueLocations={[]} - verticalBuffer={0} - /> - import java.util.ArrayList;", - "coverageStatus": "covered", - "coveredConditions": 2, - "duplicated": false, - "isNew": true, - "line": 6, - "scmAuthor": "simon.brandhof@sonarsource.com", - "scmDate": "2018-12-11T10:48:39+0100", - "scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0", - } - } - loadDuplications={[MockFunction]} - onIssueSelect={[Function]} - onIssueUnselect={[Function]} - onIssuesClose={[Function]} - onIssuesOpen={[Function]} - onLocationSelect={[MockFunction]} - onSymbolClick={[MockFunction]} - openIssues={false} - previousLine={ - { - "code": "import java.util.ArrayList;", - "coverageStatus": "covered", - "coveredConditions": 2, - "duplicated": false, - "isNew": true, - "line": 5, - "scmAuthor": "simon.brandhof@sonarsource.com", - "scmDate": "2018-12-11T10:48:39+0100", - "scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0", - } - } - renderDuplicationPopup={[MockFunction]} - secondaryIssueLocations={[]} - verticalBuffer={0} - /> - import java.util.ArrayList;", - "coverageStatus": "covered", - "coveredConditions": 2, - "duplicated": false, - "isNew": true, - "line": 7, - "scmAuthor": "simon.brandhof@sonarsource.com", - "scmDate": "2018-12-11T10:48:39+0100", - "scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0", - } - } - loadDuplications={[MockFunction]} - onIssueSelect={[Function]} - onIssueUnselect={[Function]} - onIssuesClose={[Function]} - onIssuesOpen={[Function]} - onLocationSelect={[MockFunction]} - onSymbolClick={[MockFunction]} - openIssues={false} - previousLine={ - { - "code": "import java.util.ArrayList;", - "coverageStatus": "covered", - "coveredConditions": 2, - "duplicated": false, - "isNew": true, - "line": 6, - "scmAuthor": "simon.brandhof@sonarsource.com", - "scmDate": "2018-12-11T10:48:39+0100", - "scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0", - } - } - renderDuplicationPopup={[MockFunction]} - secondaryIssueLocations={[]} - verticalBuffer={0} - /> - -
-
- -
-
-
-`; diff --git a/server/sonar-web/src/main/js/apps/issues/test-utils.tsx b/server/sonar-web/src/main/js/apps/issues/test-utils.tsx new file mode 100644 index 00000000000..cae0215f918 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/issues/test-utils.tsx @@ -0,0 +1,105 @@ +/* + * SonarQube + * Copyright (C) 2009-2023 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 { waitFor } from '@testing-library/react'; +import React from 'react'; +import { byLabelText, byRole } from 'testing-library-selector'; +import ComponentsServiceMock from '../../api/mocks/ComponentsServiceMock'; +import IssuesServiceMock from '../../api/mocks/IssuesServiceMock'; +import { mockComponent } from '../../helpers/mocks/component'; +import { mockCurrentUser } from '../../helpers/testMocks'; +import { renderApp, renderAppWithComponentContext } from '../../helpers/testReactTestingUtils'; +import { Component } from '../../types/types'; +import { CurrentUser } from '../../types/users'; +import IssuesApp from './components/IssuesApp'; +import { projectIssuesRoutes } from './routes'; + +jest.mock('../../api/issues'); +jest.mock('../../api/rules'); +jest.mock('../../api/components'); +jest.mock('../../api/users'); + +export const issuesHandler = new IssuesServiceMock(); +export const componentsHandler = new ComponentsServiceMock(); + +export const ui = { + loading: byLabelText('loading'), + issueItems: byRole('region'), + + issueItem1: byRole('region', { name: 'Issue with no location message' }), + issueItem2: byRole('region', { name: 'FlowIssue' }), + issueItem3: byRole('region', { name: 'Issue on file' }), + issueItem4: byRole('region', { name: 'Fix this' }), + issueItem5: byRole('region', { name: 'Fix that' }), + issueItem6: byRole('region', { name: 'Second issue' }), + issueItem7: byRole('region', { name: 'Issue with tags' }), + issueItem8: byRole('region', { name: 'Issue on page 2' }), + + clearIssueTypeFacet: byRole('button', { name: 'clear_x_filter.issues.facet.types' }), + codeSmellIssueTypeFilter: byRole('checkbox', { name: 'issue.type.CODE_SMELL' }), + vulnerabilityIssueTypeFilter: byRole('checkbox', { name: 'issue.type.VULNERABILITY' }), + clearSeverityFacet: byRole('button', { name: 'clear_x_filter.issues.facet.severities' }), + majorSeverityFilter: byRole('checkbox', { name: 'severity.MAJOR' }), + scopeFacet: byRole('button', { name: 'issues.facet.scopes' }), + clearScopeFacet: byRole('button', { name: 'clear_x_filter.issues.facet.scopes' }), + mainScopeFilter: byRole('checkbox', { name: 'issue.scope.MAIN' }), + resolutionFacet: byRole('button', { name: 'issues.facet.resolutions' }), + clearResolutionFacet: byRole('button', { name: 'clear_x_filter.issues.facet.resolutions' }), + fixedResolutionFilter: byRole('checkbox', { name: 'issue.resolution.FIXED' }), + statusFacet: byRole('button', { name: 'issues.facet.statuses' }), + creationDateFacet: byRole('button', { name: 'issues.facet.createdAt' }), + clearCreationDateFacet: byRole('button', { name: 'clear_x_filter.issues.facet.createdAt' }), + clearStatusFacet: byRole('button', { name: 'clear_x_filter.issues.facet.statuses' }), + openStatusFilter: byRole('checkbox', { name: 'issue.status.OPEN' }), + confirmedStatusFilter: byRole('checkbox', { name: 'issue.status.CONFIRMED' }), + ruleFacet: byRole('button', { name: 'issues.facet.rules' }), + clearRuleFacet: byRole('button', { name: 'clear_x_filter.issues.facet.rules' }), + tagFacet: byRole('button', { name: 'issues.facet.tags' }), + clearTagFacet: byRole('button', { name: 'clear_x_filter.issues.facet.tags' }), + projectFacet: byRole('button', { name: 'issues.facet.projects' }), + clearProjectFacet: byRole('button', { name: 'clear_x_filter.issues.facet.projects' }), + assigneeFacet: byRole('button', { name: 'issues.facet.assignees' }), + clearAssigneeFacet: byRole('button', { name: 'clear_x_filter.issues.facet.assignees' }), + authorFacet: byRole('button', { name: 'issues.facet.authors' }), + clearAuthorFacet: byRole('button', { name: 'clear_x_filter.issues.facet.authors' }), + + dateInputMonthSelect: byRole('combobox', { name: 'Month:' }), + dateInputYearSelect: byRole('combobox', { name: 'Year:' }), + + clearAllFilters: byRole('button', { name: 'clear_all_filters' }), +}; + +export async function waitOnDataLoaded() { + await waitFor(() => { + expect(ui.loading.query()).not.toBeInTheDocument(); + }); +} + +export function renderIssueApp(currentUser?: CurrentUser) { + renderApp('project/issues', , { currentUser: mockCurrentUser(currentUser) }); +} + +export function renderProjectIssuesApp(navigateTo?: string, overrides?: Partial) { + renderAppWithComponentContext( + 'project/issues', + projectIssuesRoutes, + { navigateTo }, + { component: mockComponent(overrides) } + ); +} -- 2.39.5