From: Wouter Admiraal Date: Tue, 28 Mar 2023 13:05:44 +0000 (+0200) Subject: SONAR-18448 Migrate the components/issue tests to RTL X-Git-Tag: 10.0.0.68432~15 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=2628add44f0d29a5a1bc15f2bcb5da4264822114;p=sonarqube.git SONAR-18448 Migrate the components/issue tests to RTL --- 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 f9f348b8a16..1809ed0dbc1 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,13 @@ * 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, times } from 'lodash'; +import { cloneDeep, keyBy, times, uniqueId } from 'lodash'; import { RuleDescriptionSections } from '../../apps/coding-rules/rule'; +import { mockIssueChangelog } from '../../helpers/mocks/issues'; import { mockSnippetsByComponent } from '../../helpers/mocks/sources'; import { RequestData } from '../../helpers/request'; import { getStandards } from '../../helpers/security-standard'; import { - mockCurrentUser, mockLoggedInUser, mockPaging, mockRawIssue, @@ -31,10 +31,12 @@ import { } from '../../helpers/testMocks'; import { ASSIGNEE_ME, + IssueActions, IssueResolution, IssueScope, IssueSeverity, IssueStatus, + IssueTransition, IssueType, RawFacet, RawIssue, @@ -49,12 +51,13 @@ import { RuleDetails, SnippetsByComponent, } from '../../types/types'; -import { NoticeType } from '../../types/users'; +import { LoggedInUser, NoticeType } from '../../types/users'; import { addIssueComment, bulkChangeIssues, deleteIssueComment, editIssueComment, + getIssueChangelog, getIssueFlowSnippets, searchIssues, searchIssueTags, @@ -97,11 +100,13 @@ interface IssueData { export default class IssuesServiceMock { isAdmin = false; + currentUser: LoggedInUser; standards?: Standards; defaultList: IssueData[]; list: IssueData[]; constructor() { + this.currentUser = mockLoggedInUser(); this.defaultList = [ { issue: mockRawIssue(false, { @@ -336,7 +341,7 @@ export default class IssuesServiceMock { }, { issue: mockRawIssue(false, { - actions: ['set_type', 'set_tags', 'comment', 'set_severity', 'assign'], + actions: Object.values(IssueActions), transitions: ['confirm', 'resolve', 'falsepositive', 'wontfix'], key: 'issue2', component: 'foo:test2.js', @@ -391,7 +396,7 @@ export default class IssuesServiceMock { }, { issue: mockRawIssue(false, { - actions: ['set_type', 'set_tags', 'comment', 'set_severity', 'assign'], + actions: Object.values(IssueActions), transitions: ['confirm', 'resolve', 'falsepositive', 'wontfix'], key: 'issue4', component: 'foo:test2.js', @@ -444,19 +449,29 @@ export default class IssuesServiceMock { (getCurrentUser as jest.Mock).mockImplementation(this.handleGetCurrentUser); (dismissNotice as jest.Mock).mockImplementation(this.handleDismissNotification); (setIssueType as jest.Mock).mockImplementation(this.handleSetIssueType); - (setIssueAssignee as jest.Mock).mockImplementation(this.handleSetIssueAssignee); + jest.mocked(setIssueAssignee).mockImplementation(this.handleSetIssueAssignee); (setIssueSeverity as jest.Mock).mockImplementation(this.handleSetIssueSeverity); (setIssueTransition as jest.Mock).mockImplementation(this.handleSetIssueTransition); (setIssueTags as jest.Mock).mockImplementation(this.handleSetIssueTags); - (addIssueComment as jest.Mock).mockImplementation(this.handleAddComment); - (editIssueComment as jest.Mock).mockImplementation(this.handleEditComment); - (deleteIssueComment as jest.Mock).mockImplementation(this.handleDeleteComment); + jest.mocked(addIssueComment).mockImplementation(this.handleAddComment); + jest.mocked(editIssueComment).mockImplementation(this.handleEditComment); + jest.mocked(deleteIssueComment).mockImplementation(this.handleDeleteComment); (searchUsers as jest.Mock).mockImplementation(this.handleSearchUsers); (searchIssueTags as jest.Mock).mockImplementation(this.handleSearchIssueTags); + jest.mocked(getIssueChangelog).mockImplementation(this.handleGetIssueChangelog); } reset = () => { this.list = cloneDeep(this.defaultList); + this.currentUser = mockLoggedInUser(); + }; + + setCurrentUser = (user: LoggedInUser) => { + this.currentUser = user; + }; + + setIssueList = (list: IssueData[]) => { + this.list = list; }; async getStandards(): Promise { @@ -703,7 +718,7 @@ export default class IssuesServiceMock { }; handleGetCurrentUser = () => { - return this.reply(mockCurrentUser()); + return this.reply(this.currentUser); }; handleDismissNotification = (noticeType: NoticeType) => { @@ -723,27 +738,45 @@ export default class IssuesServiceMock { }; handleSetIssueAssignee = (data: { issue: string; assignee?: string }) => { - return this.getActionsResponse({ assignee: data.assignee }, data.issue); + return this.getActionsResponse( + { assignee: data.assignee === '_me' ? this.currentUser.login : data.assignee }, + data.issue + ); }; handleSetIssueTransition = (data: { issue: string; transition: string }) => { - const statusMap: { [key: string]: string } = { - confirm: 'CONFIRMED', - unconfirm: 'REOPENED', - resolve: 'RESOLVED', - wontfix: 'RESOLVED', - falsepositive: 'RESOLVED', + const statusMap: { [key: string]: IssueStatus } = { + [IssueTransition.Confirm]: IssueStatus.Confirmed, + [IssueTransition.UnConfirm]: IssueStatus.Reopened, + [IssueTransition.Resolve]: IssueStatus.Resolved, + [IssueTransition.WontFix]: IssueStatus.Resolved, + [IssueTransition.FalsePositive]: IssueStatus.Resolved, }; - const transitionMap: Dict = { - REOPENED: ['confirm', 'resolve', 'falsepositive', 'wontfix'], - OPEN: ['confirm', 'resolve', 'falsepositive', 'wontfix'], - CONFIRMED: ['resolve', 'unconfirm', 'falsepositive', 'wontfix'], - RESOLVED: ['reopen'], + const transitionMap: Dict = { + [IssueStatus.Reopened]: [ + IssueTransition.Confirm, + IssueTransition.Resolve, + IssueTransition.FalsePositive, + IssueTransition.WontFix, + ], + [IssueStatus.Open]: [ + IssueTransition.Confirm, + IssueTransition.Resolve, + IssueTransition.FalsePositive, + IssueTransition.WontFix, + ], + [IssueStatus.Confirmed]: [ + IssueTransition.Resolve, + IssueTransition.UnConfirm, + IssueTransition.FalsePositive, + IssueTransition.WontFix, + ], + [IssueStatus.Resolved]: [IssueTransition.Reopen], }; const resolutionMap: Dict = { - wontfix: IssueResolution.WontFix, - falsepositive: IssueResolution.FalsePositive, + [IssueTransition.WontFix]: IssueResolution.WontFix, + [IssueTransition.FalsePositive]: IssueResolution.FalsePositive, }; return this.getActionsResponse( @@ -762,14 +795,13 @@ export default class IssuesServiceMock { }; handleAddComment = (data: { issue: string; text: string }) => { - // For comment its little more complex to get comment Id return this.getActionsResponse( { comments: [ { createdAt: '2022-07-28T11:30:04+0200', htmlText: data.text, - key: '1234', + key: uniqueId(), login: 'admin', markdown: data.text, updatable: true, @@ -781,31 +813,40 @@ export default class IssuesServiceMock { }; handleEditComment = (data: { comment: string; text: string }) => { - // For comment its little more complex to get comment Id + const issueKey = this.list.find((i) => i.issue.comments?.some((c) => c.key === data.comment)) + ?.issue.key; + if (!issueKey) { + throw new Error(`Couldn't find issue for comment ${data.comment}`); + } return this.getActionsResponse( { comments: [ { createdAt: '2022-07-28T11:30:04+0200', htmlText: data.text, - key: '1234', + key: data.comment, login: 'admin', markdown: data.text, updatable: true, }, ], }, - 'issue2' + issueKey ); }; - handleDeleteComment = () => { - // For comment its little more complex to get comment Id + handleDeleteComment = (data: { comment: string }) => { + const issue = this.list.find((i) => + i.issue.comments?.some((c) => c.key === data.comment) + )?.issue; + if (!issue) { + throw new Error(`Couldn't find issue for comment ${data.comment}`); + } return this.getActionsResponse( { - comments: [], + comments: issue.comments?.filter((c) => c.key !== data.comment), }, - 'issue2' + issue.key ); }; @@ -817,6 +858,33 @@ export default class IssuesServiceMock { return this.reply(['accessibility', 'android']); }; + handleGetIssueChangelog = (_issue: string) => { + return this.reply({ + changelog: [ + mockIssueChangelog({ + creationDate: '2018-09-01', + diffs: [ + { + key: 'status', + newValue: IssueStatus.Reopened, + oldValue: IssueStatus.Confirmed, + }, + ], + }), + mockIssueChangelog({ + creationDate: '2018-10-01', + diffs: [ + { + key: 'assign', + newValue: 'darth.vader', + oldValue: 'luke.skywalker', + }, + ], + }), + ], + }); + }; + getActionsResponse = (overrides: Partial, issueKey: string) => { const issueDataSelected = this.list.find((l) => l.issue.key === issueKey); 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 2cef9341265..fbe22d96bd0 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 @@ -290,8 +290,10 @@ describe('issues app', () => { // Improve this to include all the bulk change fonctionality it('should be able to bulk change', async () => { const user = userEvent.setup(); + const currentUser = mockLoggedInUser(); issuesHandler.setIsAdmin(true); - renderIssueApp(mockLoggedInUser()); + issuesHandler.setCurrentUser(currentUser); + renderIssueApp(currentUser); // Check that the bulk button has correct behavior expect(screen.getByRole('button', { name: 'bulk_change' })).toBeDisabled(); @@ -481,7 +483,9 @@ describe('issues app', () => { it('should allow to set creation date', async () => { const user = userEvent.setup(); - renderIssueApp(mockLoggedInUser()); + const currentUser = mockLoggedInUser(); + issuesHandler.setCurrentUser(currentUser); + renderIssueApp(currentUser); await waitOnDataLoaded(); // Select a specific date range such that only one issue matches @@ -503,7 +507,9 @@ describe('issues app', () => { it('should allow to only show my issues', async () => { const user = userEvent.setup(); - renderIssueApp(mockLoggedInUser()); + const currentUser = mockLoggedInUser(); + issuesHandler.setCurrentUser(currentUser); + renderIssueApp(currentUser); await waitOnDataLoaded(); // By default, it should show all issues @@ -895,7 +901,7 @@ describe('issues item', () => { expect( screen.getByRole('heading', { - name: 'Issue with tags sonar-lint-icon issue.resolution.badge.DEPRECATED', + name: 'Issue with tags issue.quick_fix_available_with_sonarlint_no_link issue.resolution.badge.DEPRECATED', }) ).toBeInTheDocument(); }); diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/MeasuresOverlay.tsx b/server/sonar-web/src/main/js/components/SourceViewer/components/MeasuresOverlay.tsx index 1fe474bf951..eec00a0a1b5 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/components/MeasuresOverlay.tsx +++ b/server/sonar-web/src/main/js/components/SourceViewer/components/MeasuresOverlay.tsx @@ -39,7 +39,8 @@ import { } from '../../../helpers/measures'; import { getBranchLikeUrl } from '../../../helpers/urls'; import { BranchLike } from '../../../types/branch-like'; -import { IssueType as IssueTypeEnum } from '../../../types/issues'; +import { IssueSeverity, IssueType as IssueTypeEnum } from '../../../types/issues'; +import { MetricType } from '../../../types/metrics'; import { FacetValue, IssueType, MeasureEnhanced, SourceViewerFile } from '../../../types/types'; import Measure from '../../measure/Measure'; import SeverityHelper from '../../shared/SeverityHelper'; @@ -199,7 +200,9 @@ export default class MeasuresOverlay extends React.PureComponent { {translate('issue.type', f.val)} - {formatMeasure(f.count, 'SHORT_INT')} + + {formatMeasure(f.count, MetricType.ShortInteger)} + ) )} @@ -207,14 +210,18 @@ export default class MeasuresOverlay extends React.PureComponent { )} {severitiesFacet && (
- {sortBy(severitiesFacet, (f) => SEVERITIES.indexOf(f.val)).map((f) => ( -
- - - - {formatMeasure(f.count, 'SHORT_INT')} -
- ))} + {sortBy(severitiesFacet, (f) => SEVERITIES.indexOf(f.val as IssueSeverity)).map( + (f) => ( +
+ + + + + {formatMeasure(f.count, MetricType.ShortInteger)} + +
+ ) + )}
)} {tagsFacet && ( @@ -225,7 +232,9 @@ export default class MeasuresOverlay extends React.PureComponent { {f.val} - {formatMeasure(f.count, 'SHORT_INT')} + + {formatMeasure(f.count, MetricType.ShortInteger)} + ))} diff --git a/server/sonar-web/src/main/js/components/issue/Issue.css b/server/sonar-web/src/main/js/components/issue/Issue.css index b9fd5495faa..5be5920c71f 100644 --- a/server/sonar-web/src/main/js/components/issue/Issue.css +++ b/server/sonar-web/src/main/js/components/issue/Issue.css @@ -31,10 +31,6 @@ cursor: initial; } -.issue.hotspot { - background-color: var(--hotspotBgColor); -} - .issue.selected, .issue-message-box.selected { box-shadow: none; diff --git a/server/sonar-web/src/main/js/components/issue/Issue.tsx b/server/sonar-web/src/main/js/components/issue/Issue.tsx index fbb18e9dd06..0dd933c582b 100644 --- a/server/sonar-web/src/main/js/components/issue/Issue.tsx +++ b/server/sonar-web/src/main/js/components/issue/Issue.tsx @@ -25,8 +25,8 @@ import { getKeyboardShortcutEnabled } from '../../helpers/preferences'; import { BranchLike } from '../../types/branch-like'; import { Issue as TypeIssue } from '../../types/types'; import { updateIssue } from './actions'; +import IssueView from './components/IssueView'; import './Issue.css'; -import IssueView from './IssueView'; interface Props { branchLike?: BranchLike; diff --git a/server/sonar-web/src/main/js/components/issue/IssueView.tsx b/server/sonar-web/src/main/js/components/issue/IssueView.tsx deleted file mode 100644 index 09a2ae2f4bb..00000000000 --- a/server/sonar-web/src/main/js/components/issue/IssueView.tsx +++ /dev/null @@ -1,156 +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 classNames from 'classnames'; -import * as React from 'react'; -import { deleteIssueComment, editIssueComment } from '../../api/issues'; -import Checkbox from '../../components/controls/Checkbox'; -import { translate, translateWithParameters } from '../../helpers/l10n'; -import { BranchLike } from '../../types/branch-like'; -import { Issue } from '../../types/types'; -import { updateIssue } from './actions'; -import IssueActionsBar from './components/IssueActionsBar'; -import IssueCommentLine from './components/IssueCommentLine'; -import IssueTitleBar from './components/IssueTitleBar'; - -interface Props { - branchLike?: BranchLike; - checked?: boolean; - currentPopup?: string; - displayWhyIsThisAnIssue?: boolean; - displayLocationsCount?: boolean; - displayLocationsLink?: boolean; - issue: Issue; - onAssign: (login: string) => void; - onChange: (issue: Issue) => void; - onCheck?: (issue: string) => void; - onClick?: (issueKey: string) => void; - onFilter?: (property: string, issue: Issue) => void; - selected: boolean; - togglePopup: (popup: string, show: boolean | void) => void; -} - -export default class IssueView extends React.PureComponent { - handleCheck = () => { - if (this.props.onCheck) { - this.props.onCheck(this.props.issue.key); - } - }; - - handleBoxClick = (event: React.MouseEvent) => { - if (!isClickable(event.target as HTMLElement) && this.props.onClick) { - event.preventDefault(); - this.handleDetailClick(); - } - }; - - handleDetailClick = () => { - if (this.props.onClick) { - this.props.onClick(this.props.issue.key); - } - }; - - editComment = (comment: string, text: string) => { - updateIssue(this.props.onChange, editIssueComment({ comment, text })); - }; - - deleteComment = (comment: string) => { - updateIssue(this.props.onChange, deleteIssueComment({ comment })); - }; - - render() { - const { - issue, - branchLike, - checked, - currentPopup, - displayWhyIsThisAnIssue, - displayLocationsLink, - displayLocationsCount, - } = this.props; - - const hasCheckbox = this.props.onCheck != null; - - const issueClass = classNames('issue', { - 'no-click': this.props.onClick === undefined, - hotspot: issue.type === 'SECURITY_HOTSPOT', - 'issue-with-checkbox': hasCheckbox, - selected: this.props.selected, - }); - - return ( -
- {hasCheckbox && ( - - )} - - - {issue.comments && issue.comments.length > 0 && ( -
- {issue.comments.map((comment) => ( - - ))} -
- )} -
- ); - } -} - -function isClickable(node: HTMLElement | undefined | null): boolean { - if (!node) { - return false; - } - const clickableTags = ['A', 'BUTTON', 'INPUT', 'TEXTAREA']; - const tagName = (node.tagName || '').toUpperCase(); - return clickableTags.includes(tagName) || isClickable(node.parentElement); -} diff --git a/server/sonar-web/src/main/js/components/issue/__tests__/Issue-it.tsx b/server/sonar-web/src/main/js/components/issue/__tests__/Issue-it.tsx new file mode 100644 index 00000000000..0f6d9799898 --- /dev/null +++ b/server/sonar-web/src/main/js/components/issue/__tests__/Issue-it.tsx @@ -0,0 +1,554 @@ +/* + * 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 { act, screen, within } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { omit, pick } from 'lodash'; +import * as React from 'react'; +import { byRole, byText } from 'testing-library-selector'; +import IssuesServiceMock from '../../../api/mocks/IssuesServiceMock'; +import { KeyboardKeys } from '../../../helpers/keycodes'; +import { mockIssueComment } from '../../../helpers/mocks/issues'; +import { mockIssue, mockLoggedInUser, mockRawIssue } from '../../../helpers/testMocks'; +import { findTooltipWithContent, renderApp } from '../../../helpers/testReactTestingUtils'; +import { + IssueActions, + IssueSeverity, + IssueStatus, + IssueTransition, + IssueType, +} from '../../../types/issues'; +import { RuleStatus } from '../../../types/rules'; +import { IssueComment } from '../../../types/types'; +import Issue from '../Issue'; + +jest.mock('../../../api/issues'); +jest.mock('../../../api/rules'); +jest.mock('../../../api/users'); + +jest.mock('../../../helpers/preferences', () => ({ + getKeyboardShortcutEnabled: jest.fn(() => true), +})); + +const issuesHandler = new IssuesServiceMock(); + +beforeEach(() => { + issuesHandler.reset(); +}); + +describe('rendering', () => { + it('should render correctly with comments', () => { + const { ui } = getPageObject(); + renderIssue({ issue: mockIssue(false, { comments: [mockIssueCommentPosted4YearsAgo()] }) }); + + const comments = within(ui.commentsList()); + expect(comments.getByText('Leïa Skywalker')).toBeInTheDocument(); + expect(comments.getByRole('listitem')).toHaveTextContent('This is a comment, bud'); + expect(comments.getByRole('listitem')).toHaveTextContent('issue.comment.posted_on4 years ago'); + }); + + it('should render correctly for locations, issue message, line, permalink, why, and effort', async () => { + const { ui } = getPageObject(); + const issue = mockIssue(true, { effort: '2 days', message: 'This is an issue' }); + const onClick = jest.fn(); + renderIssue({ issue, displayLocationsCount: true, displayWhyIsThisAnIssue: true, onClick }); + + expect(ui.locationsBadge(7).get()).toBeInTheDocument(); + expect(ui.lineInfo(26).get()).toBeInTheDocument(); + expect(ui.permalink.get()).toHaveAttribute( + 'href', + `/project/issues?issues=${issue.key}&open=${issue.key}&id=${issue.project}` + ); + expect(ui.whyLink.get()).toBeInTheDocument(); + expect(ui.effort('2 days').get()).toBeInTheDocument(); + await ui.clickIssueMessage(); + expect(onClick).toHaveBeenCalledWith(issue.key); + }); + + it.each([RuleStatus.Deprecated, RuleStatus.Removed])( + 'should render correctly for a %s rule', + (ruleStatus) => { + const { ui } = getPageObject(); + renderIssue({ issue: mockIssue(false, { ruleStatus }) }); + expect(ui.ruleStatusBadge(ruleStatus).get()).toBeInTheDocument(); + } + ); + + it('should render correctly for external rule engines', () => { + renderIssue({ issue: mockIssue(true, { externalRuleEngine: 'ESLINT' }) }); + expect(screen.getByText('ESLINT')).toBeInTheDocument(); + }); + + it('should render the SonarLint icon correctly', () => { + renderIssue({ issue: mockIssue(false, { quickFixAvailable: true }) }); + expect( + findTooltipWithContent('issue.quick_fix_available_with_sonarlint_no_link') + ).toBeInTheDocument(); + }); + + it('should render correctly with a checkbox', async () => { + const { ui } = getPageObject(); + const onCheck = jest.fn(); + const issue = mockIssue(); + renderIssue({ onCheck, issue }); + await ui.toggleCheckbox(); + expect(onCheck).toHaveBeenCalledWith(issue.key); + }); + + it('should correctly render the changelog', async () => { + const { ui } = getPageObject(); + renderIssue(); + + await ui.showChangelog(); + expect( + ui.changelogRow('status', IssueStatus.Confirmed, IssueStatus.Reopened).get() + ).toBeInTheDocument(); + expect(ui.changelogRow('assign', 'luke.skywalker', 'darth.vader').get()).toBeInTheDocument(); + }); +}); + +describe('updating', () => { + it('should allow updating the type', async () => { + const { ui } = getPageObject(); + const issue = mockRawIssue(false, { + type: IssueType.Bug, + actions: [IssueActions.SetType], + }); + issuesHandler.setIssueList([{ issue, snippets: {} }]); + renderIssue({ issue: mockIssue(false, { ...pick(issue, 'actions', 'key', 'type') }) }); + + await ui.updateType(IssueType.Bug, IssueType.CodeSmell); + expect(ui.updateTypeBtn(IssueType.CodeSmell).get()).toBeInTheDocument(); + }); + + it('should allow updating the severity', async () => { + const { ui } = getPageObject(); + const issue = mockRawIssue(false, { + severity: IssueSeverity.Blocker, + actions: [IssueActions.SetSeverity], + }); + issuesHandler.setIssueList([{ issue, snippets: {} }]); + renderIssue({ issue: mockIssue(false, { ...pick(issue, 'actions', 'key', 'severity') }) }); + + await ui.updateSeverity(IssueSeverity.Blocker, IssueSeverity.Minor); + expect(ui.updateSeverityBtn(IssueSeverity.Minor).get()).toBeInTheDocument(); + }); + + it('should allow updating the status', async () => { + const { ui } = getPageObject(); + const issue = mockRawIssue(false, { + status: IssueStatus.Open, + transitions: [IssueTransition.Confirm, IssueTransition.UnConfirm], + }); + issuesHandler.setIssueList([{ issue, snippets: {} }]); + renderIssue({ + issue: mockIssue(false, { ...pick(issue, 'key', 'status', 'transitions') }), + }); + + await ui.updateStatus(IssueStatus.Open, IssueTransition.Confirm); + expect(ui.updateStatusBtn(IssueStatus.Confirmed).get()).toBeInTheDocument(); + }); + + it('should allow assigning', async () => { + const { ui } = getPageObject(); + const issue = mockRawIssue(false, { + assignee: 'leia', + actions: [IssueActions.Assign], + }); + issuesHandler.setIssueList([{ issue, snippets: {} }]); + renderIssue({ + issue: mockIssue(false, { ...pick(issue, 'actions', 'key', 'assignee') }), + }); + + await ui.updateAssignee('leia', 'Skywalker'); + expect(ui.updateAssigneeBtn('luke').get()).toBeInTheDocument(); + }); + + it('should allow commenting', async () => { + const { ui } = getPageObject(); + const issue = mockRawIssue(false, { + actions: [IssueActions.Comment], + }); + issuesHandler.setIssueList([{ issue, snippets: {} }]); + renderIssue({ issue: mockIssue(false, { ...pick(issue, 'actions', 'key') }) }); + + // Create + await ui.addComment('Original content'); + const comments = within(ui.commentsList()); + expect(comments.getByRole('listitem')).toHaveTextContent('Original content'); + + // Update + await ui.updateComment('New content'); + expect(comments.getByRole('listitem')).toHaveTextContent('New content'); + + // Delete + await ui.deleteComment(); + expect(comments.getByRole('listitem')).toHaveTextContent('New content'); + }); + + it('should allow updating the tags', async () => { + const { ui } = getPageObject(); + const issue = mockRawIssue(false, { + tags: [], + actions: [IssueActions.SetTags], + }); + issuesHandler.setIssueList([{ issue, snippets: {} }]); + renderIssue({ issue: mockIssue(false, { ...pick(issue, 'actions', 'key', 'tags') }) }); + + await ui.addTag('accessibility'); + await ui.addTag('android', ['accessibility']); + expect(ui.updateTagsBtn(['accessibility', 'android']).get()).toBeInTheDocument(); + }); +}); + +it('should correctly handle keyboard shortcuts', async () => { + const { ui } = getPageObject(); + const onCheck = jest.fn(); + const issue = mockRawIssue(false, { + actions: Object.values(IssueActions), + assignee: 'luke', + transitions: [IssueTransition.Confirm, IssueTransition.UnConfirm], + }); + issuesHandler.setIssueList([{ issue, snippets: {} }]); + issuesHandler.setCurrentUser(mockLoggedInUser({ login: 'leia', name: 'Organa' })); + renderIssue({ + onCheck, + selected: true, + issue: mockIssue(false, { ...pick(issue, 'actions', 'key', 'assignee', 'transitions') }), + }); + + await ui.pressTransitionShortcut(); + expect(ui.setStatusBtn(IssueTransition.UnConfirm).get()).toBeInTheDocument(); + await ui.pressDismissShortcut(); + + await ui.pressAssignShortcut(); + expect(ui.setAssigneeBtn(/Organa/).get()).toBeInTheDocument(); + await ui.pressDismissShortcut(); + + await ui.pressSeverityShortcut(); + expect(ui.setSeverityBtn(IssueSeverity.Minor).get()).toBeInTheDocument(); + await ui.pressDismissShortcut(); + + await ui.pressCommentShortcut(); + expect(ui.commentTextInput.get()).toBeInTheDocument(); + await ui.pressDismissShortcut(); + + await ui.pressTagsShortcut(); + expect(ui.tagsSearchInput.get()).toBeInTheDocument(); + await ui.pressDismissShortcut(); + + await ui.pressCheckShortcut(); + expect(onCheck).toHaveBeenCalled(); + + expect(ui.updateAssigneeBtn('luke').get()).toBeInTheDocument(); + await ui.pressAssignToMeShortcut(); + expect(ui.updateAssigneeBtn('leia').get()).toBeInTheDocument(); +}); + +it('should correctly handle similar issues filtering', async () => { + const { ui, user } = getPageObject(); + const onFilter = jest.fn(); + const issue = mockIssue(false, { + ruleName: 'Rule Foo', + tags: ['accessibility', 'owasp'], + projectName: 'Project Bar', + componentLongName: 'main.js', + }); + renderIssue({ onFilter, issue }); + + await ui.showSimilarIssues(); + await user.click(ui.similarIssueTypeLink.get()); + expect(onFilter).toHaveBeenLastCalledWith('type', issue); + + await ui.showSimilarIssues(); + await user.click(ui.similarIssueSeverityLink.get()); + expect(onFilter).toHaveBeenLastCalledWith('severity', issue); + + await ui.showSimilarIssues(); + await user.click(ui.similarIssueStatusLink.get()); + expect(onFilter).toHaveBeenLastCalledWith('status', issue); + + await ui.showSimilarIssues(); + await user.click(ui.similarIssueResolutionLink.get()); + expect(onFilter).toHaveBeenLastCalledWith('resolution', issue); + + await ui.showSimilarIssues(); + await user.click(ui.similarIssueAssigneeLink.get()); + expect(onFilter).toHaveBeenLastCalledWith('assignee', issue); + + await ui.showSimilarIssues(); + await user.click(ui.similarIssueRuleLink.get()); + expect(onFilter).toHaveBeenLastCalledWith('rule', issue); + + await ui.showSimilarIssues(); + await user.click(ui.similarIssueTagLink('accessibility').get()); + expect(onFilter).toHaveBeenLastCalledWith('tag###accessibility', issue); + + await ui.showSimilarIssues(); + await user.click(ui.similarIssueTagLink('owasp').get()); + expect(onFilter).toHaveBeenLastCalledWith('tag###owasp', issue); + + await ui.showSimilarIssues(); + await user.click(ui.similarIssueProjectLink.get()); + expect(onFilter).toHaveBeenLastCalledWith('project', issue); + + await ui.showSimilarIssues(); + await user.click(ui.similarIssueFileLink.get()); + expect(onFilter).toHaveBeenLastCalledWith('file', issue); +}); + +function mockIssueCommentPosted4YearsAgo(overrides: Partial = {}) { + const date = new Date(); + date.setFullYear(date.getFullYear() - 4); + return mockIssueComment({ + authorName: 'Leïa Skywalker', + createdAt: date.toISOString(), + ...overrides, + }); +} + +function getPageObject() { + const user = userEvent.setup(); + + const selectors = { + // Issue + ruleStatusBadge: (status: RuleStatus) => byText(`issue.resolution.badge.${status}`), + locationsBadge: (count: number) => byText(count), + lineInfo: (line: number) => byText(`L${line}`), + permalink: byRole('link', { name: 'permalink' }), + effort: (effort: string) => byText(`issue.x_effort.${effort}`), + whyLink: byRole('link', { name: 'issue.why_this_issue.long' }), + checkbox: byRole('checkbox'), + issueMessageBtn: byRole('button', { name: 'This is an issue' }), + + // Changelog + toggleChangelogBtn: byRole('button', { + name: /issue.changelog.found_on_x_show_more/, + }), + changelogRow: (key: string, oldValue: string, newValue: string) => + byRole('row', { + name: new RegExp( + `issue\\.changelog\\.changed_to\\.issue\\.changelog\\.field\\.${key}\\.${newValue} \\(issue\\.changelog\\.was\\.${oldValue}\\)` + ), + }), + + // Similar issues + toggleSimilarIssuesBtn: byRole('button', { name: 'issue.filter_similar_issues' }), + similarIssueTypeLink: byRole('button', { name: 'issue.type.BUG' }), + similarIssueSeverityLink: byRole('button', { name: 'severity.MAJOR' }), + similarIssueStatusLink: byRole('button', { name: 'issue.status.OPEN' }), + similarIssueResolutionLink: byRole('button', { name: 'unresolved' }), + similarIssueAssigneeLink: byRole('button', { name: 'unassigned' }), + similarIssueRuleLink: byRole('button', { name: 'Rule Foo' }), + similarIssueTagLink: (name: string) => byRole('button', { name }), + similarIssueProjectLink: byRole('button', { name: 'qualifier.TRK Project Bar' }), + similarIssueFileLink: byRole('button', { name: 'qualifier.FIL main.js' }), + + // Comment + commentsList: () => { + const list = byRole('list') + .getAll() + .find((el) => el.getAttribute('data-testid') === 'issue-comments'); + if (list === undefined) { + throw new Error('Could not find comments list'); + } + return list; + }, + commentAddBtn: byRole('button', { name: 'issue.comment.add_comment' }), + commentEditBtn: byRole('button', { name: 'issue.comment.edit' }), + commentTextInput: byRole('textbox', { name: 'issue.comment.enter_comment' }), + commentSaveBtn: byRole('button', { name: 'issue.comment.formlink' }), + commentUpdateBtn: byRole('button', { name: 'save' }), + commentDeleteBtn: byRole('button', { name: 'issue.comment.delete' }), + commentConfirmDeleteBtn: byRole('button', { name: 'delete' }), + + // Type + updateTypeBtn: (currentType: IssueType) => + byRole('button', { name: `issue.type.type_x_click_to_change.issue.type.${currentType}` }), + setTypeBtn: (type: IssueType) => byRole('button', { name: `issue.type.${type}` }), + + // Severity + updateSeverityBtn: (currentSeverity: IssueSeverity) => + byRole('button', { + name: `issue.severity.severity_x_click_to_change.severity.${currentSeverity}`, + }), + setSeverityBtn: (severity: IssueSeverity) => byRole('button', { name: `severity.${severity}` }), + + // Status + updateStatusBtn: (currentStatus: IssueStatus) => + byRole('button', { + name: `issue.transition.status_x_click_to_change.issue.status.${currentStatus}`, + }), + setStatusBtn: (transition: IssueTransition) => + byRole('button', { name: `issue.transition.${transition}` }), + + // Assignee + assigneeSearchInput: byRole('searchbox'), + updateAssigneeBtn: (currentAssignee: string) => + byRole('button', { + name: `issue.assign.assigned_to_x_click_to_change.${currentAssignee}`, + }), + setAssigneeBtn: (name: RegExp) => byRole('button', { name }), + + // Tags + tagsSearchInput: byRole('searchbox'), + updateTagsBtn: (currentTags?: string[]) => + byRole('button', { + name: `tags_list_x.${currentTags ? currentTags.join(', ') : 'issue.no_tag'}`, + }), + toggleTagCheckbox: (name: string) => byRole('checkbox', { name }), + }; + + const ui = { + ...selectors, + async addComment(content: string) { + await user.click(selectors.commentAddBtn.get()); + await user.type(selectors.commentTextInput.get(), content); + await act(async () => { + await user.click(selectors.commentSaveBtn.get()); + }); + }, + async updateComment(content: string) { + await user.click(selectors.commentEditBtn.get()); + await user.type(selectors.commentTextInput.get(), content); + await act(async () => { + await user.keyboard(`{Control>}{${KeyboardKeys.Enter}}{/Control}`); + }); + }, + async deleteComment() { + await user.click(selectors.commentDeleteBtn.get()); + await act(async () => { + await user.click(selectors.commentConfirmDeleteBtn.get()); + }); + }, + async updateType(currentType: IssueType, newType: IssueType) { + await user.click(selectors.updateTypeBtn(currentType).get()); + await act(async () => { + await user.click(selectors.setTypeBtn(newType).get()); + }); + }, + async updateSeverity(currentSeverity: IssueSeverity, newSeverity: IssueSeverity) { + await user.click(selectors.updateSeverityBtn(currentSeverity).get()); + await act(async () => { + await user.click(selectors.setSeverityBtn(newSeverity).get()); + }); + }, + async updateStatus(currentStatus: IssueStatus, transition: IssueTransition) { + await user.click(selectors.updateStatusBtn(currentStatus).get()); + await act(async () => { + await user.click(selectors.setStatusBtn(transition).get()); + }); + }, + async updateAssignee(currentAssignee: string, newAssignee: string) { + await user.click(selectors.updateAssigneeBtn(currentAssignee).get()); + await user.type(selectors.assigneeSearchInput.get(), newAssignee); + await act(async () => { + await user.click(selectors.setAssigneeBtn(new RegExp(newAssignee)).get()); + }); + }, + async addTag(tag: string, currentTagList?: string[]) { + await user.click(selectors.updateTagsBtn(currentTagList).get()); + await act(async () => { + await user.click(selectors.toggleTagCheckbox(tag).get()); + }); + await act(async () => { + await user.keyboard('{Escape}'); + }); + }, + async showChangelog() { + await user.click(selectors.toggleChangelogBtn.get()); + }, + async showSimilarIssues() { + await user.click(selectors.toggleSimilarIssuesBtn.get()); + }, + async toggleCheckbox() { + await user.click(selectors.checkbox.get()); + }, + async clickIssueMessage() { + await user.click(selectors.issueMessageBtn.get()); + }, + async pressDismissShortcut() { + await act(async () => { + await user.keyboard(`{${KeyboardKeys.Escape}}`); + }); + }, + async pressTransitionShortcut() { + await act(async () => { + await user.keyboard(`{${KeyboardKeys.KeyF}}`); + }); + }, + async pressAssignShortcut() { + await act(async () => { + await user.keyboard(`{${KeyboardKeys.KeyA}}`); + }); + }, + async pressAssignToMeShortcut() { + await act(async () => { + await user.keyboard(`{${KeyboardKeys.KeyM}}`); + }); + }, + async pressSeverityShortcut() { + await act(async () => { + await user.keyboard(`{${KeyboardKeys.KeyI}}`); + }); + }, + async pressCommentShortcut() { + await act(async () => { + await user.keyboard(`{${KeyboardKeys.KeyC}}`); + }); + }, + async pressTagsShortcut() { + await act(async () => { + await user.keyboard(`{${KeyboardKeys.KeyT}}`); + }); + }, + async pressCheckShortcut() { + await act(async () => { + await user.keyboard(`{${KeyboardKeys.Space}}`); + }); + }, + }; + + return { ui, user }; +} + +function renderIssue(props: Partial> = {}) { + function Wrapper(wrapperProps: Omit) { + const [issue, setIssue] = React.useState(wrapperProps.issue); + const [openPopup, setOpenPopup] = React.useState(); + return ( + { + setIssue({ ...issue, ...newIssue }); + }} + onPopupToggle={(_key, popup, open) => { + setOpenPopup(open === false ? undefined : popup); + }} + {...omit(wrapperProps, 'issue')} + /> + ); + } + + return renderApp('/', , { + currentUser: mockLoggedInUser({ login: 'leia', name: 'Organa' }), + }); +} diff --git a/server/sonar-web/src/main/js/components/issue/__tests__/IssueView-test.tsx b/server/sonar-web/src/main/js/components/issue/__tests__/IssueView-test.tsx deleted file mode 100644 index a1e3b0f26ce..00000000000 --- a/server/sonar-web/src/main/js/components/issue/__tests__/IssueView-test.tsx +++ /dev/null @@ -1,62 +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 { mockIssue } from '../../../helpers/testMocks'; -import IssueView from '../IssueView'; - -it('should render issues correctly', () => { - expect(shallowRender()).toMatchSnapshot(); -}); - -it('should render hotspots correctly', () => { - expect( - shallowRender({ issue: mockIssue(false, { type: 'SECURITY_HOTSPOT' }) }) - ).toMatchSnapshot(); -}); - -function shallowRender(props: Partial = {}) { - return shallow( - - ); -} diff --git a/server/sonar-web/src/main/js/components/issue/__tests__/__snapshots__/IssueView-test.tsx.snap b/server/sonar-web/src/main/js/components/issue/__tests__/__snapshots__/IssueView-test.tsx.snap deleted file mode 100644 index bb33e7066d4..00000000000 --- a/server/sonar-web/src/main/js/components/issue/__tests__/__snapshots__/IssueView-test.tsx.snap +++ /dev/null @@ -1,224 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`should render hotspots correctly 1`] = ` -
- - -
-`; - -exports[`should render issues correctly 1`] = ` -
- - -
- -
-
-`; diff --git a/server/sonar-web/src/main/js/components/issue/__tests__/__snapshots__/issue-test.tsx.snap b/server/sonar-web/src/main/js/components/issue/__tests__/__snapshots__/issue-test.tsx.snap deleted file mode 100644 index 655c1f68805..00000000000 --- a/server/sonar-web/src/main/js/components/issue/__tests__/__snapshots__/issue-test.tsx.snap +++ /dev/null @@ -1,62 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`should render issues correctly 1`] = ` - -`; diff --git a/server/sonar-web/src/main/js/components/issue/__tests__/issue-test.tsx b/server/sonar-web/src/main/js/components/issue/__tests__/issue-test.tsx deleted file mode 100644 index d80be59ccb8..00000000000 --- a/server/sonar-web/src/main/js/components/issue/__tests__/issue-test.tsx +++ /dev/null @@ -1,113 +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 { KeyboardKeys } from '../../../helpers/keycodes'; -import { mockIssue } from '../../../helpers/testMocks'; -import { keydown } from '../../../helpers/testUtils'; -import Issue from '../Issue'; - -it('should render issues correctly', () => { - expect(shallowRender()).toMatchSnapshot(); -}); - -it('should call the proper function with the proper props when pressing shortcuts (FAMICT)', () => { - const onPopupToggle = jest.fn(); - const onCheck = jest.fn(); - const issue = mockIssue(false, { - comments: [ - { - key: '1', - htmlText: 'My comment', - markdown: 'My comment', - updatable: false, - createdAt: '2017-07-05T09:33:29+0200', - author: 'admin', - authorLogin: 'admin', - authorName: 'Admin', - authorAvatar: 'admin-avatar', - authorActive: true, - }, - ], - actions: ['assign'], - }); - - shallowRender({ onPopupToggle, issue, onCheck }); - keydown({ key: KeyboardKeys.KeyF, metaKey: true }); - expect(onPopupToggle).not.toHaveBeenCalledWith(issue.key, 'transition', undefined); - - keydown({ key: KeyboardKeys.KeyF }); - expect(onPopupToggle).toHaveBeenCalledWith(issue.key, 'transition', undefined); - - keydown({ key: KeyboardKeys.KeyA }); - expect(onPopupToggle).toHaveBeenCalledWith(issue.key, 'assign', undefined); - keydown({ key: KeyboardKeys.Escape }); - - keydown({ key: KeyboardKeys.KeyM }); - expect(onPopupToggle).toHaveBeenCalledWith(issue.key, 'assign', false); - - keydown({ key: KeyboardKeys.KeyI }); - expect(onPopupToggle).toHaveBeenCalledWith(issue.key, 'set-severity', undefined); - - keydown({ key: KeyboardKeys.KeyC, metaKey: true }); - expect(onPopupToggle).not.toHaveBeenCalledWith(issue.key, 'comment', undefined); - - keydown({ key: KeyboardKeys.KeyC }); - expect(onPopupToggle).toHaveBeenCalledWith(issue.key, 'comment', undefined); - keydown({ key: KeyboardKeys.Escape }); - - keydown({ key: KeyboardKeys.KeyT }); - expect(onPopupToggle).toHaveBeenCalledWith(issue.key, 'edit-tags', undefined); - - keydown({ key: KeyboardKeys.Space }); - expect(onCheck).toHaveBeenCalledWith(issue.key); -}); - -function shallowRender(props: Partial = {}) { - return shallow( - - ); -} diff --git a/server/sonar-web/src/main/js/components/issue/components/IssueActionsBar.tsx b/server/sonar-web/src/main/js/components/issue/components/IssueActionsBar.tsx index 3856c5489dd..1153e95b6c8 100644 --- a/server/sonar-web/src/main/js/components/issue/components/IssueActionsBar.tsx +++ b/server/sonar-web/src/main/js/components/issue/components/IssueActionsBar.tsx @@ -20,7 +20,12 @@ import classNames from 'classnames'; import * as React from 'react'; import { translate, translateWithParameters } from '../../../helpers/l10n'; -import { IssueResolution, IssueResponse, IssueType as IssueTypeEnum } from '../../../types/issues'; +import { + IssueActions, + IssueResolution, + IssueResponse, + IssueType as IssueTypeEnum, +} from '../../../types/issues'; import { Issue, RawQuery } from '../../../types/types'; import { updateIssue } from '../actions'; import IssueAssign from './IssueAssign'; @@ -90,13 +95,12 @@ export default class IssueActionsBar extends React.PureComponent { render() { const { issue, className, showCommentsInPopup } = this.props; - const canAssign = issue.actions.includes('assign'); - const canComment = issue.actions.includes('comment'); - const canSetSeverity = issue.actions.includes('set_severity'); - const canSetType = issue.actions.includes('set_type'); - const canSetTags = issue.actions.includes('set_tags'); - const hasTransitions = issue.transitions && issue.transitions.length > 0; - const isSecurityHotspot = issue.type === IssueTypeEnum.SecurityHotspot; + const canAssign = issue.actions.includes(IssueActions.Assign); + const canComment = issue.actions.includes(IssueActions.Comment); + const canSetSeverity = issue.actions.includes(IssueActions.SetSeverity); + const canSetType = issue.actions.includes(IssueActions.SetType); + const canSetTags = issue.actions.includes(IssueActions.SetTags); + const hasTransitions = issue.transitions.length > 0; return (
@@ -110,17 +114,15 @@ export default class IssueActionsBar extends React.PureComponent { togglePopup={this.props.togglePopup} />
- {!isSecurityHotspot && ( -
- -
- )} +
+ +
{ togglePopup={this.props.togglePopup} />
- {!isSecurityHotspot && issue.effort && ( + {issue.effort && (
{translateWithParameters('issue.x_effort', issue.effort)} diff --git a/server/sonar-web/src/main/js/components/issue/components/IssueChangelog.tsx b/server/sonar-web/src/main/js/components/issue/components/IssueChangelog.tsx index 474292dd00c..0807942f041 100644 --- a/server/sonar-web/src/main/js/components/issue/components/IssueChangelog.tsx +++ b/server/sonar-web/src/main/js/components/issue/components/IssueChangelog.tsx @@ -18,53 +18,58 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; +import { injectIntl, WrappedComponentProps } from 'react-intl'; import { ButtonLink } from '../../../components/controls/buttons'; import Toggler from '../../../components/controls/Toggler'; import DropdownIcon from '../../../components/icons/DropdownIcon'; +import { translateWithParameters } from '../../../helpers/l10n'; import { Issue } from '../../../types/types'; +import { formatterOption } from '../../intl/DateFormatter'; import DateFromNow from '../../intl/DateFromNow'; import ChangelogPopup from '../popups/ChangelogPopup'; -interface Props { +export interface IssueChangelogProps extends WrappedComponentProps { isOpen: boolean; issue: Pick; creationDate: string; togglePopup: (popup: string, show?: boolean) => void; } -export default class IssueChangelog extends React.PureComponent { - toggleChangelog = (open?: boolean) => { - this.props.togglePopup('changelog', open); - }; - - handleClick = () => { - this.toggleChangelog(); - }; - - handleClose = () => { - this.toggleChangelog(false); - }; - - render() { - return ( -
- } +function IssueChangelog(props: IssueChangelogProps) { + const { + isOpen, + issue, + creationDate, + intl: { formatDate }, + } = props; + return ( +
+ { + props.togglePopup('changelog', false); + }} + open={isOpen} + overlay={} + > + { + props.togglePopup('changelog'); + }} > - - - - - - - -
- ); - } + + + + + +
+
+ ); } + +export default injectIntl(IssueChangelog); diff --git a/server/sonar-web/src/main/js/components/issue/components/IssueChangelogDiff.tsx b/server/sonar-web/src/main/js/components/issue/components/IssueChangelogDiff.tsx index 476af1ec357..fdb0c7a6d2d 100644 --- a/server/sonar-web/src/main/js/components/issue/components/IssueChangelogDiff.tsx +++ b/server/sonar-web/src/main/js/components/issue/components/IssueChangelogDiff.tsx @@ -22,18 +22,18 @@ import { translate, translateWithParameters } from '../../../helpers/l10n'; import { formatMeasure } from '../../../helpers/measures'; import { IssueChangelogDiff as TypeIssueChangelogDiff } from '../../../types/types'; -interface Props { +export interface IssueChangelogDiffProps { diff: TypeIssueChangelogDiff; } -export default function IssueChangelogDiff({ diff }: Props) { +export default function IssueChangelogDiff({ diff }: IssueChangelogDiffProps) { if (diff.key === 'file') { return (

{translateWithParameters( 'issue.change.file_move', - diff.oldValue || '', - diff.newValue || '' + diff.oldValue ?? '', + diff.newValue ?? '' )}

); @@ -42,8 +42,8 @@ export default function IssueChangelogDiff({ diff }: Props) {

{translateWithParameters( 'issue.change.from_branch', - diff.oldValue || '', - diff.newValue || '' + diff.oldValue ?? '', + diff.newValue ?? '' )}

); @@ -53,13 +53,13 @@ export default function IssueChangelogDiff({ diff }: Props) {

{translateWithParameters( 'issue.change.from_non_branch', - diff.oldValue || '', - diff.newValue || '' + diff.oldValue ?? '', + diff.newValue ?? '' )}

); } else if (diff.key === 'line') { - return

{translateWithParameters('issue.changelog.line_removed_X', diff.oldValue || '')}

; + return

{translateWithParameters('issue.changelog.line_removed_X', diff.oldValue ?? '')}

; } let message; diff --git a/server/sonar-web/src/main/js/components/issue/components/IssueCommentLine.tsx b/server/sonar-web/src/main/js/components/issue/components/IssueCommentLine.tsx index e60f161c599..08ec3edc3cf 100644 --- a/server/sonar-web/src/main/js/components/issue/components/IssueCommentLine.tsx +++ b/server/sonar-web/src/main/js/components/issue/components/IssueCommentLine.tsx @@ -85,7 +85,7 @@ export default class IssueCommentLine extends React.PureComponent ? translateWithParameters('user.x_deleted', author) : author; return ( -
+
  • )}
  • -
    + ); } } diff --git a/server/sonar-web/src/main/js/components/issue/components/IssueMessageTags.tsx b/server/sonar-web/src/main/js/components/issue/components/IssueMessageTags.tsx index a4ab9133b34..5bf6636996f 100644 --- a/server/sonar-web/src/main/js/components/issue/components/IssueMessageTags.tsx +++ b/server/sonar-web/src/main/js/components/issue/components/IssueMessageTags.tsx @@ -37,7 +37,7 @@ export default function IssueMessageTags(props: IssueMessageTagsProps) { const { engine, quickFixAvailable, ruleStatus } = props; const { externalRulesRepoNames } = React.useContext(WorkspaceContext); - const ruleEngine = (engine && externalRulesRepoNames && externalRulesRepoNames[engine]) || engine; + const ruleEngine = (engine && externalRulesRepoNames[engine]) || engine; return ( <> @@ -64,7 +64,7 @@ export default function IssueMessageTags(props: IssueMessageTagsProps) { )} diff --git a/server/sonar-web/src/main/js/components/issue/components/IssueTitleBar.tsx b/server/sonar-web/src/main/js/components/issue/components/IssueTitleBar.tsx index 881e8b34924..4df5ca5a8e7 100644 --- a/server/sonar-web/src/main/js/components/issue/components/IssueTitleBar.tsx +++ b/server/sonar-web/src/main/js/components/issue/components/IssueTitleBar.tsx @@ -26,6 +26,8 @@ import { translate, translateWithParameters } from '../../../helpers/l10n'; import { formatMeasure } from '../../../helpers/measures'; import { getComponentIssuesUrl } from '../../../helpers/urls'; import { BranchLike } from '../../../types/branch-like'; +import { IssueType } from '../../../types/issues'; +import { MetricType } from '../../../types/metrics'; import { Issue } from '../../../types/types'; import LocationIndex from '../../common/LocationIndex'; import IssueChangelog from './IssueChangelog'; @@ -57,7 +59,7 @@ export default function IssueTitleBar(props: IssueTitleBarProps) { {locationsCount} @@ -70,7 +72,7 @@ export default function IssueTitleBar(props: IssueTitleBarProps) { ...getBranchLikeQuery(props.branchLike), issues: issue.key, open: issue.key, - types: issue.type === 'SECURITY_HOTSPOT' ? issue.type : undefined, + types: issue.type === IssueType.SecurityHotspot ? issue.type : undefined, }); return ( diff --git a/server/sonar-web/src/main/js/components/issue/components/IssueView.tsx b/server/sonar-web/src/main/js/components/issue/components/IssueView.tsx new file mode 100644 index 00000000000..ccc675fae26 --- /dev/null +++ b/server/sonar-web/src/main/js/components/issue/components/IssueView.tsx @@ -0,0 +1,155 @@ +/* + * 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 classNames from 'classnames'; +import * as React from 'react'; +import { deleteIssueComment, editIssueComment } from '../../../api/issues'; +import { translate, translateWithParameters } from '../../../helpers/l10n'; +import { BranchLike } from '../../../types/branch-like'; +import { Issue } from '../../../types/types'; +import Checkbox from '../../controls/Checkbox'; +import { updateIssue } from '../actions'; +import IssueActionsBar from './IssueActionsBar'; +import IssueCommentLine from './IssueCommentLine'; +import IssueTitleBar from './IssueTitleBar'; + +interface Props { + branchLike?: BranchLike; + checked?: boolean; + currentPopup?: string; + displayWhyIsThisAnIssue?: boolean; + displayLocationsCount?: boolean; + displayLocationsLink?: boolean; + issue: Issue; + onAssign: (login: string) => void; + onChange: (issue: Issue) => void; + onCheck?: (issue: string) => void; + onClick?: (issueKey: string) => void; + onFilter?: (property: string, issue: Issue) => void; + selected: boolean; + togglePopup: (popup: string, show: boolean | void) => void; +} + +export default class IssueView extends React.PureComponent { + handleCheck = () => { + if (this.props.onCheck) { + this.props.onCheck(this.props.issue.key); + } + }; + + handleBoxClick = (event: React.MouseEvent) => { + if (!isClickable(event.target as HTMLElement) && this.props.onClick) { + event.preventDefault(); + this.handleDetailClick(); + } + }; + + handleDetailClick = () => { + if (this.props.onClick) { + this.props.onClick(this.props.issue.key); + } + }; + + editComment = (comment: string, text: string) => { + updateIssue(this.props.onChange, editIssueComment({ comment, text })); + }; + + deleteComment = (comment: string) => { + updateIssue(this.props.onChange, deleteIssueComment({ comment })); + }; + + render() { + const { + issue, + branchLike, + checked, + currentPopup, + displayWhyIsThisAnIssue, + displayLocationsLink, + displayLocationsCount, + } = this.props; + + const hasCheckbox = this.props.onCheck != null; + + const issueClass = classNames('issue', { + 'no-click': this.props.onClick === undefined, + 'issue-with-checkbox': hasCheckbox, + selected: this.props.selected, + }); + + return ( +
    + {hasCheckbox && ( + + )} + + + {issue.comments && issue.comments.length > 0 && ( +
      + {issue.comments.map((comment) => ( + + ))} +
    + )} +
    + ); + } +} + +function isClickable(node: HTMLElement | undefined | null): boolean { + if (!node) { + return false; + } + const clickableTags = ['A', 'BUTTON', 'INPUT', 'TEXTAREA']; + const tagName = (node.tagName || '').toUpperCase(); + return clickableTags.includes(tagName) || isClickable(node.parentElement); +} diff --git a/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueActionsBar-test.tsx b/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueActionsBar-test.tsx deleted file mode 100644 index 191d56553bd..00000000000 --- a/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueActionsBar-test.tsx +++ /dev/null @@ -1,124 +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 { mockIssue } from '../../../../helpers/testMocks'; -import { Issue } from '../../../../types/types'; -import IssueActionsBar from '../IssueActionsBar'; - -jest.mock('../../actions', () => ({ updateIssue: jest.fn() })); - -it('should render issue correctly', () => { - const element = shallow( - - ); - expect(element).toMatchSnapshot(); -}); - -it('should render security hotspot correctly', () => { - const element = shallow( - - ); - expect(element).toMatchSnapshot(); -}); - -it('should render commentable correctly', () => { - const element = shallow( - - ); - expect(element).toMatchSnapshot(); -}); - -it('should render effort correctly', () => { - const element = shallow( - - ); - expect(element).toMatchSnapshot(); -}); - -describe('callback', () => { - const issue: Issue = mockIssue(); - const onChangeMock = jest.fn(); - const togglePopupMock = jest.fn(); - - const element = shallow( - - ); - - beforeEach(() => { - jest.clearAllMocks(); - }); - - it('handleTransition should call onChange', () => { - const instance = element.instance(); - instance.handleTransition(issue); - expect(onChangeMock).toHaveBeenCalledTimes(1); - expect(togglePopupMock).toHaveBeenCalledTimes(0); - }); - - it('setIssueProperty should call togglePopup', () => { - const instance = element.instance(); - - const apiCallMock = jest.fn(); - - instance.setIssueProperty('author', 'popup', apiCallMock, 'Jay'); - expect(togglePopupMock).toHaveBeenCalledTimes(1); - expect(apiCallMock).toHaveBeenCalledTimes(1); - }); - - it('toggleComment should call togglePopup and update state', () => { - const instance = element.instance(); - - expect(element.state('commentAutoTriggered')).toBe(false); - expect(element.state('commentPlaceholder')).toBe(''); - - instance.toggleComment(false, 'hold my place', true); - - expect(element.state('commentAutoTriggered')).toBe(true); - expect(element.state('commentPlaceholder')).toBe('hold my place'); - - expect(togglePopupMock).toHaveBeenCalledTimes(1); - }); -}); diff --git a/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueAssign-test.tsx b/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueAssign-test.tsx deleted file mode 100644 index bb077adde87..00000000000 --- a/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueAssign-test.tsx +++ /dev/null @@ -1,66 +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 { mockIssue } from '../../../../helpers/testMocks'; -import { click } from '../../../../helpers/testUtils'; -import IssueAssign from '../IssueAssign'; - -const issue = mockIssue(false, { - assignee: 'john', - assigneeAvatar: 'gravatarhash', - assigneeName: 'John Doe', -}); - -it('should render without the action when the correct rights are missing', () => { - expect(shallowRender({ canAssign: false })).toMatchSnapshot(); -}); - -it('should render with the action', () => { - expect(shallowRender()).toMatchSnapshot(); -}); - -it('should render a fallback assignee display if assignee info are not available', () => { - expect( - shallowRender({ issue: mockIssue(false, { assignee: undefined, assigneeName: undefined }) }) - ).toMatchSnapshot(); -}); - -it('should open the popup when the button is clicked', () => { - const togglePopup = jest.fn(); - const element = shallowRender({ togglePopup }); - click(element.find('ButtonLink')); - expect(togglePopup.mock.calls).toMatchSnapshot(); - element.setProps({ isOpen: true }); - expect(element).toMatchSnapshot(); -}); - -function shallowRender(props: Partial = {}) { - return shallow( - - ); -} diff --git a/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueChangelog-test.tsx b/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueChangelog-test.tsx deleted file mode 100644 index 40f4c3eb9a8..00000000000 --- a/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueChangelog-test.tsx +++ /dev/null @@ -1,55 +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 { click } from '../../../../helpers/testUtils'; -import IssueChangelog from '../IssueChangelog'; - -const issue = { - key: 'issuekey', - author: 'john.david.dalton@gmail.com', - creationDate: '2017-03-01T09:36:01+0100', -}; - -it('should render correctly', () => { - const element = shallowRender(); - expect(element).toMatchSnapshot(); -}); - -it('should open the popup when the button is clicked', () => { - const togglePopup = jest.fn(); - const element = shallowRender({ togglePopup }); - click(element.find('ButtonLink')); - expect(togglePopup.mock.calls).toMatchSnapshot(); - element.setProps({ isOpen: true }); - expect(element).toMatchSnapshot(); -}); - -function shallowRender(props: Partial = {}) { - return shallow( - - ); -} diff --git a/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueChangelogDiff-test.tsx b/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueChangelogDiff-test.tsx index e159697ab45..179ecf35bf5 100644 --- a/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueChangelogDiff-test.tsx +++ b/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueChangelogDiff-test.tsx @@ -17,67 +17,56 @@ * 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 { screen } from '@testing-library/react'; import * as React from 'react'; -import { IssueChangelogDiff as TypeIssueChangelogDiff } from '../../../../types/types'; -import IssueChangelogDiff from '../IssueChangelogDiff'; +import { mockIssueChangelogDiff } from '../../../../helpers/mocks/issues'; +import { renderComponent } from '../../../../helpers/testReactTestingUtils'; +import IssueChangelogDiff, { IssueChangelogDiffProps } from '../IssueChangelogDiff'; -it('should render correctly', () => { - expect(shallowRender()).toMatchSnapshot(); -}); +jest.mock('../../../../helpers/measures', () => ({ + formatMeasure: jest + .fn() + .mockImplementation((value: string, type: string) => `formatted.${value}.as.${type}`), +})); -it('should render correctly file diff', () => { - expect( - shallowRender({ diff: { key: 'file', oldValue: 'foo/bar.js', newValue: 'bar/baz.js' } }) - ).toMatchSnapshot(); -}); +it.each([ + ['file', 'issue.change.file_move.oldValue.newValue', undefined], + ['from_branch', 'issue.change.from_branch.oldValue.newValue', undefined], + ['line', 'issue.changelog.line_removed_X.oldValue', undefined], + [ + 'effort', + 'issue.changelog.changed_to.issue.changelog.field.effort.formatted.12.as.WORK_DUR', + { newValue: '12', oldValue: undefined }, + ], + [ + 'effort', + 'issue.changelog.removed.issue.changelog.field.effort (issue.changelog.was.formatted.14.as.WORK_DUR)', + { newValue: undefined, oldValue: '14' }, + ], + [ + 'effort', + 'issue.changelog.removed.issue.changelog.field.effort', + { newValue: undefined, oldValue: undefined }, + ], + [ + 'assign', + 'issue.changelog.changed_to.issue.changelog.field.assign.newValue (issue.changelog.was.oldValue)', + undefined, + ], + ['from_short_branch', 'issue.change.from_non_branch.oldValue.newValue', undefined], -it('should render correctly branch diff', () => { - expect( - shallowRender({ - diff: { - // Legacy key - key: 'from_long_branch', - oldValue: 'foo', - newValue: 'bar', - }, - }) - ).toMatchSnapshot(); + // This should be deprecated. Can this still happen? + ['from_long_branch', 'issue.change.from_branch.oldValue.newValue', undefined], +])( + 'should render correctly for "%s" diff types', + (key, expected, diff?: Partial) => { + renderIssueChangelogDiff({ + diff: mockIssueChangelogDiff({ key, newValue: 'newValue', oldValue: 'oldValue', ...diff }), + }); + expect(screen.getByText(expected)).toBeInTheDocument(); + } +); - expect( - shallowRender({ - diff: { - // Legacy key - key: 'from_short_branch', - oldValue: 'foo', - newValue: 'bar', - }, - }) - ).toMatchSnapshot(); - - expect( - shallowRender({ - diff: { - key: 'from_branch', - oldValue: 'foo', - newValue: 'bar', - }, - }) - ).toMatchSnapshot(); -}); - -it('should render correctly line diff', () => { - expect(shallowRender({ diff: { key: 'line', oldValue: '80' } })).toMatchSnapshot(); -}); - -it('should render correctly effort diff', () => { - expect(shallowRender({ diff: { key: 'effort', newValue: '12' } })).toMatchSnapshot(); - expect( - shallowRender({ diff: { key: 'effort', newValue: '12', oldValue: '10' } }) - ).toMatchSnapshot(); - expect(shallowRender({ diff: { key: 'effort', oldValue: '10' } })).toMatchSnapshot(); -}); - -function shallowRender(props: Partial<{ diff: TypeIssueChangelogDiff }> = {}) { - return shallow(); +function renderIssueChangelogDiff(props: Partial = {}) { + return renderComponent(); } diff --git a/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueCommentLine-test.tsx b/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueCommentLine-test.tsx deleted file mode 100644 index 98a33404ae8..00000000000 --- a/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueCommentLine-test.tsx +++ /dev/null @@ -1,68 +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 { click } from '../../../../helpers/testUtils'; -import { IssueComment } from '../../../../types/types'; -import IssueCommentLine from '../IssueCommentLine'; - -const comment: IssueComment = { - author: 'john.doe', - authorActive: true, - authorAvatar: 'gravatarhash', - authorName: 'John Doe', - createdAt: '2017-03-01T09:36:01+0100', - htmlText: 'test', - key: 'comment-key', - markdown: '*test*', - updatable: true, -}; - -it('should render correctly a comment that is updatable', () => { - expect(shallowRender()).toMatchSnapshot(); -}); - -it('should render correctly a comment that is not updatable', () => { - expect(shallowRender({ comment: { ...comment, updatable: false } })).toMatchSnapshot(); -}); - -it('should open the right popups when the buttons are clicked', () => { - const wrapper = shallowRender(); - click(wrapper.find('.js-issue-comment-edit')); - expect(wrapper.state()).toMatchSnapshot(); - click(wrapper.find('.js-issue-comment-delete')); - expect(wrapper.state()).toMatchSnapshot(); - wrapper.update(); - expect(wrapper).toMatchSnapshot(); -}); - -it('should render correctly a comment with a deleted author', () => { - expect( - shallowRender({ - comment: { ...comment, authorActive: false, authorName: undefined }, - }).find('.issue-comment-author') - ).toMatchSnapshot(); -}); - -function shallowRender(props: Partial = {}) { - return shallow( - - ); -} diff --git a/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueMessage-test.tsx b/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueMessage-test.tsx deleted file mode 100644 index 8cea8e54049..00000000000 --- a/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueMessage-test.tsx +++ /dev/null @@ -1,66 +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 { mockBranch } from '../../../../helpers/mocks/branch-like'; -import { mockIssue } from '../../../../helpers/testMocks'; -import { RuleStatus } from '../../../../types/rules'; -import IssueMessage, { IssueMessageProps } from '../IssueMessage'; - -jest.mock('react', () => { - return { - ...jest.requireActual('react'), - useContext: jest - .fn() - .mockImplementation(() => ({ externalRulesRepoNames: {}, openRule: jest.fn() })), - }; -}); - -it('should render correctly', () => { - expect(shallowRender()).toMatchSnapshot('default'); - expect(shallowRender({ issue: mockIssue(false, { externalRuleEngine: 'js' }) })).toMatchSnapshot( - 'with engine info' - ); - expect(shallowRender({ issue: mockIssue(false, { quickFixAvailable: true }) })).toMatchSnapshot( - 'with quick fix' - ); - expect( - shallowRender({ issue: mockIssue(false, { ruleStatus: RuleStatus.Deprecated }) }) - ).toMatchSnapshot('is deprecated rule'); - expect( - shallowRender({ issue: mockIssue(false, { ruleStatus: RuleStatus.Removed }) }) - ).toMatchSnapshot('is removed rule'); - expect(shallowRender({ displayWhyIsThisAnIssue: false })).toMatchSnapshot( - 'hide why is it an issue' - ); -}); - -function shallowRender(props: Partial = {}) { - return shallow( - - ); -} diff --git a/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueSeverity-test.tsx b/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueSeverity-test.tsx deleted file mode 100644 index f175342e297..00000000000 --- a/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueSeverity-test.tsx +++ /dev/null @@ -1,55 +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 { click } from '../../../../helpers/testUtils'; -import IssueSeverity from '../IssueSeverity'; - -const issue = { severity: 'BLOCKER' }; - -it('should render without the action when the correct rights are missing', () => { - expect(shallowRender({ canSetSeverity: false })).toMatchSnapshot(); -}); - -it('should render with the action', () => { - expect(shallowRender()).toMatchSnapshot(); -}); - -it('should open the popup when the button is clicked', () => { - const togglePopup = jest.fn(); - const element = shallowRender({ togglePopup }); - click(element.find('ButtonLink')); - expect(togglePopup.mock.calls).toMatchSnapshot(); - element.setProps({ isOpen: true }); - expect(element).toMatchSnapshot(); -}); - -function shallowRender(props: Partial = {}) { - return shallow( - - ); -} diff --git a/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueTitleBar-test.tsx b/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueTitleBar-test.tsx deleted file mode 100644 index 0a433daac04..00000000000 --- a/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueTitleBar-test.tsx +++ /dev/null @@ -1,50 +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 { mockBranch } from '../../../../helpers/mocks/branch-like'; -import { mockIssue } from '../../../../helpers/testMocks'; -import IssueTitleBar, { IssueTitleBarProps } from '../IssueTitleBar'; - -it('should render correctly', () => { - expect(shallowRender()).toMatchSnapshot('default'); - expect(shallowRender({ onFilter: jest.fn() })).toMatchSnapshot('with filter'); - expect(shallowRender({ displayLocationsCount: true, issue: mockIssue(true) })).toMatchSnapshot( - 'with multi locations' - ); - expect( - shallowRender({ - branchLike: mockBranch(), - displayLocationsCount: true, - displayLocationsLink: true, - issue: mockIssue(true), - }) - ).toMatchSnapshot('with multi locations and link'); -}); - -function shallowRender(props: Partial = {}) { - return shallow( - - ); -} diff --git a/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueTransition-test.tsx b/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueTransition-test.tsx deleted file mode 100644 index e765a0830dc..00000000000 --- a/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueTransition-test.tsx +++ /dev/null @@ -1,51 +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 IssueTransition from '../IssueTransition'; - -const issue: IssueTransition['props']['issue'] = { - key: 'foo1234', - transitions: ['confirm', 'resolve', 'falsepositive', 'wontfix'], - status: 'OPEN', - type: 'BUG', -}; - -it('should render without the action when there is no transitions', () => { - expect( - shallowRender({ - hasTransitions: false, - issue: { key: 'foo1234', transitions: [], status: 'CLOSED', type: 'BUG' }, - }) - ).toMatchSnapshot(); -}); - -function shallowRender(props: Partial = {}) { - return shallow( - - ); -} diff --git a/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueType-test.tsx b/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueType-test.tsx deleted file mode 100644 index b5d0a5fa92f..00000000000 --- a/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueType-test.tsx +++ /dev/null @@ -1,42 +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 { Issue } from '../../../../types/types'; -import IssueType from '../IssueType'; - -const issue: Pick = { type: 'BUG' }; - -it('should render without the action when the correct rights are missing', () => { - expect(shallowRender({ canSetType: false })).toMatchSnapshot(); -}); - -function shallowRender(props: Partial = {}) { - return shallow( - - ); -} diff --git a/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueActionsBar-test.tsx.snap b/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueActionsBar-test.tsx.snap deleted file mode 100644 index 0a5febfcea3..00000000000 --- a/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueActionsBar-test.tsx.snap +++ /dev/null @@ -1,910 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`should render commentable correctly 1`] = ` -
    -
    -
    - -
    -
    - -
    -
    - -
    -
    - -
    - -
    -
    -
    - -
    -
    -
    -`; - -exports[`should render effort correctly 1`] = ` -
    -
    -
    - -
    -
    - -
    -
    - -
    -
    - -
    -
    - - issue.x_effort.great - -
    -
    -
    -
    - -
    -
    -
    -`; - -exports[`should render issue correctly 1`] = ` -
    -
    -
    - -
    -
    - -
    -
    - -
    -
    - -
    -
    -
    -
    - -
    -
    -
    -`; - -exports[`should render security hotspot correctly 1`] = ` -
    -
    -
    - -
    -
    - -
    -
    - -
    -
    -
    -
    - -
    -
    -
    -`; diff --git a/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueAssign-test.tsx.snap b/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueAssign-test.tsx.snap deleted file mode 100644 index 5a25cdc3ba2..00000000000 --- a/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueAssign-test.tsx.snap +++ /dev/null @@ -1,152 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`should open the popup when the button is clicked 1`] = ` -[ - [ - "assign", - undefined, - ], -] -`; - -exports[`should open the popup when the button is clicked 2`] = ` -
    - - } - > - - - - - - John Doe - - - - -
    -`; - -exports[`should render a fallback assignee display if assignee info are not available 1`] = ` -
    - - } - > - - - unassigned - - - - -
    -`; - -exports[`should render with the action 1`] = ` -
    - - } - > - - - - - - John Doe - - - - -
    -`; - -exports[`should render without the action when the correct rights are missing 1`] = ` - - - - - - John Doe - - -`; diff --git a/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueChangelog-test.tsx.snap b/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueChangelog-test.tsx.snap deleted file mode 100644 index 3cb8bc76968..00000000000 --- a/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueChangelog-test.tsx.snap +++ /dev/null @@ -1,88 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`should open the popup when the button is clicked 1`] = ` -[ - [ - "changelog", - undefined, - ], -] -`; - -exports[`should open the popup when the button is clicked 2`] = ` -
    - - } - > - - - - - - - -
    -`; - -exports[`should render correctly 1`] = ` -
    - - } - > - - - - - - - -
    -`; diff --git a/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueChangelogDiff-test.tsx.snap b/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueChangelogDiff-test.tsx.snap deleted file mode 100644 index 1e953fa8a4e..00000000000 --- a/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueChangelogDiff-test.tsx.snap +++ /dev/null @@ -1,55 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`should render correctly 1`] = ` -

    - issue.changelog.removed.issue.changelog.field.foo -

    -`; - -exports[`should render correctly branch diff 1`] = ` -

    - issue.change.from_branch.foo.bar -

    -`; - -exports[`should render correctly branch diff 2`] = ` -

    - issue.change.from_non_branch.foo.bar -

    -`; - -exports[`should render correctly branch diff 3`] = ` -

    - issue.change.from_branch.foo.bar -

    -`; - -exports[`should render correctly effort diff 1`] = ` -

    - issue.changelog.changed_to.issue.changelog.field.effort.work_duration.x_minutes.12 -

    -`; - -exports[`should render correctly effort diff 2`] = ` -

    - issue.changelog.changed_to.issue.changelog.field.effort.work_duration.x_minutes.12 (issue.changelog.was.work_duration.x_minutes.10) -

    -`; - -exports[`should render correctly effort diff 3`] = ` -

    - issue.changelog.removed.issue.changelog.field.effort (issue.changelog.was.work_duration.x_minutes.10) -

    -`; - -exports[`should render correctly file diff 1`] = ` -

    - issue.change.file_move.foo/bar.js.bar/baz.js -

    -`; - -exports[`should render correctly line diff 1`] = ` -

    - issue.changelog.line_removed_X.80 -

    -`; diff --git a/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueCommentLine-test.tsx.snap b/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueCommentLine-test.tsx.snap deleted file mode 100644 index 563f86aac29..00000000000 --- a/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueCommentLine-test.tsx.snap +++ /dev/null @@ -1,266 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`should open the right popups when the buttons are clicked 1`] = ` -{ - "openPopup": "edit", -} -`; - -exports[`should open the right popups when the buttons are clicked 2`] = ` -{ - "openPopup": "delete", -} -`; - -exports[`should open the right popups when the buttons are clicked 3`] = ` -
    -
    - - John Doe -
    -
    test", - } - } - /> -
    - - issue.comment.posted_on - - -
    -
    -
    - test", - "key": "comment-key", - "markdown": "*test*", - "updatable": true, - } - } - onComment={[Function]} - placeholder="" - placement="bottom-right" - toggleComment={[Function]} - /> - } - > - - -
    -
    - - } - > - - -
    -
    -
    -`; - -exports[`should render correctly a comment that is not updatable 1`] = ` -
    -
    - - John Doe -
    -
    test", - } - } - /> -
    - - issue.comment.posted_on - - -
    -
    -
    -`; - -exports[`should render correctly a comment that is updatable 1`] = ` -
    -
    - - John Doe -
    -
    test", - } - } - /> -
    - - issue.comment.posted_on - - -
    -
    -
    - test", - "key": "comment-key", - "markdown": "*test*", - "updatable": true, - } - } - onComment={[Function]} - placeholder="" - placement="bottom-right" - toggleComment={[Function]} - /> - } - > - - -
    -
    - - } - > - - -
    -
    -
    -`; - -exports[`should render correctly a comment with a deleted author 1`] = ` -
    - - user.x_deleted.john.doe -
    -`; diff --git a/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueMessage-test.tsx.snap b/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueMessage-test.tsx.snap deleted file mode 100644 index 22c41aacb22..00000000000 --- a/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueMessage-test.tsx.snap +++ /dev/null @@ -1,181 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`should render correctly: default 1`] = ` - -
    - - - - -
    - - issue.why_this_issue - -
    -`; - -exports[`should render correctly: hide why is it an issue 1`] = ` - -
    - - - - -
    -
    -`; - -exports[`should render correctly: is deprecated rule 1`] = ` - -
    - - - - -
    - - issue.why_this_issue - -
    -`; - -exports[`should render correctly: is removed rule 1`] = ` - -
    - - - - -
    - - issue.why_this_issue - -
    -`; - -exports[`should render correctly: with engine info 1`] = ` - -
    - - - - -
    - - issue.why_this_issue - -
    -`; - -exports[`should render correctly: with quick fix 1`] = ` - -
    - - - - -
    - - issue.why_this_issue - -
    -`; diff --git a/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueSeverity-test.tsx.snap b/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueSeverity-test.tsx.snap deleted file mode 100644 index c2d685b6b3c..00000000000 --- a/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueSeverity-test.tsx.snap +++ /dev/null @@ -1,89 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`should open the popup when the button is clicked 1`] = ` -[ - [ - "set-severity", - undefined, - ], -] -`; - -exports[`should open the popup when the button is clicked 2`] = ` -
    - - } - > - - - - - -
    -`; - -exports[`should render with the action 1`] = ` -
    - - } - > - - - - - -
    -`; - -exports[`should render without the action when the correct rights are missing 1`] = ` - -`; diff --git a/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueTitleBar-test.tsx.snap b/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueTitleBar-test.tsx.snap deleted file mode 100644 index 75e63851efb..00000000000 --- a/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueTitleBar-test.tsx.snap +++ /dev/null @@ -1,849 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`should render correctly: default 1`] = ` -
    - -
    -
    -
    - -
    -
    - - L - 26 - -
    -
    - - - -
    -
    -
    -
    -`; - -exports[`should render correctly: with filter 1`] = ` -
    - -
    -
    -
    - -
    -
    - - L - 26 - -
    -
    - - - -
    -
    - -
    -
    -
    -
    -`; - -exports[`should render correctly: with multi locations 1`] = ` -
    - -
    -
    -
    - -
    -
    - - L - 26 - -
    -
    - - - 7 - - -
    -
    - - - -
    -
    -
    -
    -`; - -exports[`should render correctly: with multi locations and link 1`] = ` -
    - -
    -
    -
    - -
    -
    - - L - 26 - -
    -
    - - - - 7 - - - -
    -
    - - - -
    -
    -
    -
    -`; diff --git a/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueTransition-test.tsx.snap b/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueTransition-test.tsx.snap deleted file mode 100644 index e4eb999534e..00000000000 --- a/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueTransition-test.tsx.snap +++ /dev/null @@ -1,8 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`should render without the action when there is no transitions 1`] = ` - -`; diff --git a/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueType-test.tsx.snap b/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueType-test.tsx.snap deleted file mode 100644 index 1078e100a75..00000000000 --- a/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueType-test.tsx.snap +++ /dev/null @@ -1,11 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`should render without the action when the correct rights are missing 1`] = ` - - - issue.type.BUG - -`; diff --git a/server/sonar-web/src/main/js/components/issue/popups/ChangelogPopup.tsx b/server/sonar-web/src/main/js/components/issue/popups/ChangelogPopup.tsx index 40b04710109..3cd9bcbb419 100644 --- a/server/sonar-web/src/main/js/components/issue/popups/ChangelogPopup.tsx +++ b/server/sonar-web/src/main/js/components/issue/popups/ChangelogPopup.tsx @@ -85,7 +85,7 @@ export default class ChangelogPopup extends React.PureComponent { -

    +

    {userName && ( <> { 'issue.changelog.webhook_source', item.webhookSource )} -

    +
    {item.diffs.map((diff) => ( ))} diff --git a/server/sonar-web/src/main/js/components/issue/popups/CommentForm.tsx b/server/sonar-web/src/main/js/components/issue/popups/CommentForm.tsx index da7b0aa6482..58bcee83f15 100644 --- a/server/sonar-web/src/main/js/components/issue/popups/CommentForm.tsx +++ b/server/sonar-web/src/main/js/components/issue/popups/CommentForm.tsx @@ -44,9 +44,9 @@ export default function CommentForm(props: CommentFormProps) { style={{ resize: 'vertical' }} placeholder={placeholder} aria-label={translate('issue.comment.enter_comment')} - onChange={(event: React.ChangeEvent) => - setEditComment(event.target.value) - } + onChange={(event: React.ChangeEvent) => { + setEditComment(event.target.value); + }} onKeyDown={(event: React.KeyboardEvent) => { if (event.nativeEvent.key === KeyboardKeys.Enter && (event.metaKey || event.ctrlKey)) { props.onSaveComment(editComment); diff --git a/server/sonar-web/src/main/js/components/issue/popups/SimilarIssuesPopup.tsx b/server/sonar-web/src/main/js/components/issue/popups/SimilarIssuesPopup.tsx index e42615b3dd2..ce5b4f27360 100644 --- a/server/sonar-web/src/main/js/components/issue/popups/SimilarIssuesPopup.tsx +++ b/server/sonar-web/src/main/js/components/issue/popups/SimilarIssuesPopup.tsx @@ -24,6 +24,7 @@ import QualifierIcon from '../../../components/icons/QualifierIcon'; import TagsIcon from '../../../components/icons/TagsIcon'; import { translate, translateWithParameters } from '../../../helpers/l10n'; import { fileFromPath, limitComponentName } from '../../../helpers/path'; +import { ComponentQualifier } from '../../../types/component'; import { Issue } from '../../../types/types'; import SelectList from '../../common/SelectList'; import SelectListItem from '../../common/SelectListItem'; @@ -31,112 +32,107 @@ import SeverityHelper from '../../shared/SeverityHelper'; import StatusHelper from '../../shared/StatusHelper'; import Avatar from '../../ui/Avatar'; -interface Props { +interface SimilarIssuesPopupProps { issue: Issue; onFilter: (property: string, issue: Issue) => void; } -export default class SimilarIssuesPopup extends React.PureComponent { - handleSelect = (property: string) => { - this.props.onFilter(property, this.props.issue); - }; - - render() { - const { issue } = this.props; - - const items = [ - 'type', - 'severity', - 'status', - 'resolution', - 'assignee', - 'rule', - ...(issue.tags || []).map((tag) => `tag###${tag}`), - 'project', - 'file', - ].filter((item) => item) as string[]; - - const assignee = issue.assigneeName || issue.assignee; - - return ( - -
    -
    {translate('issue.filter_similar_issues')}
    -
    - - - - - {translate('issue.type', issue.type)} +export default function SimilarIssuesPopup(props: SimilarIssuesPopupProps) { + const { issue } = props; + + const items = [ + 'type', + 'severity', + 'status', + 'resolution', + 'assignee', + 'rule', + ...(issue.tags ?? []).map((tag) => `tag###${tag}`), + 'project', + 'file', + ].filter((item) => item) as string[]; + + const assignee = issue.assigneeName ?? issue.assignee; + + return ( + +
    +
    {translate('issue.filter_similar_issues')}
    +
    + + { + props.onFilter(property, issue); + }} + > + + + {translate('issue.type', issue.type)} + + + + + + + + + + + + {issue.resolution != null + ? translate('issue.resolution', issue.resolution) + : translate('unresolved')} + + + + {assignee ? ( + + {translate('assigned_to')} + + {issue.assigneeActive === false + ? translateWithParameters('user.x_deleted', assignee) + : assignee} + + ) : ( + translate('unassigned') + )} + + +
  • + + {limitComponentName(issue.ruleName)} + + {issue.tags?.map((tag) => ( + + + {tag} - - - - - - - - - - - {issue.resolution != null - ? translate('issue.resolution', issue.resolution) - : translate('unresolved')} - - - - {assignee ? ( - - {translate('assigned_to')} - - {issue.assigneeActive === false - ? translateWithParameters('user.x_deleted', assignee) - : assignee} - - ) : ( - translate('unassigned') - )} - - -
  • - - {limitComponentName(issue.ruleName)} - - {issue.tags != null && - issue.tags.map((tag) => ( - - - {tag} - - ))} - -
  • - - - - {issue.projectName} - - - - - {fileFromPath(issue.componentLongName)} - - - - ); - } + ))} + +
  • + + + + {issue.projectName} + + + + + {fileFromPath(issue.componentLongName)} + + + + ); } diff --git a/server/sonar-web/src/main/js/components/issue/popups/__tests__/ChangelogPopup-test.tsx b/server/sonar-web/src/main/js/components/issue/popups/__tests__/ChangelogPopup-test.tsx deleted file mode 100644 index fc2923c9dfe..00000000000 --- a/server/sonar-web/src/main/js/components/issue/popups/__tests__/ChangelogPopup-test.tsx +++ /dev/null @@ -1,132 +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 { getIssueChangelog } from '../../../../api/issues'; -import { waitAndUpdate } from '../../../../helpers/testUtils'; -import ChangelogPopup from '../ChangelogPopup'; - -jest.mock('../../../../api/issues', () => ({ - getIssueChangelog: jest.fn().mockResolvedValue({ - changelog: [ - { - creationDate: '2017-03-01T09:36:01+0100', - user: 'john.doe', - isUserActive: true, - userName: 'John Doe', - avatar: 'gravatarhash', - diffs: [{ key: 'severity', newValue: 'MINOR', oldValue: 'CRITICAL' }], - }, - ], - }), -})); - -beforeEach(() => { - jest.clearAllMocks(); -}); - -it('should render the changelog popup correctly', async () => { - const wrapper = shallowRender(); - await waitAndUpdate(wrapper); - expect(getIssueChangelog).toHaveBeenCalledWith('issuekey'); - expect(wrapper).toMatchSnapshot(); -}); - -it('should render the changelog popup when we have a deleted user', async () => { - (getIssueChangelog as jest.Mock).mockResolvedValueOnce({ - changelog: [ - { - creationDate: '2017-03-01T09:36:01+0100', - user: 'john.doe', - isUserActive: false, - diffs: [{ key: 'severity', newValue: 'MINOR', oldValue: 'CRITICAL' }], - }, - ], - }); - const wrapper = shallowRender(); - await waitAndUpdate(wrapper); - expect(wrapper).toMatchSnapshot(); -}); - -it('should render the changelog popup when change was triggered by a webhook with external user', async () => { - (getIssueChangelog as jest.Mock).mockResolvedValueOnce({ - changelog: [ - { - creationDate: '2017-03-01T09:36:01+0100', - user: null, - isUserActive: false, - diffs: [{ key: 'severity', newValue: 'MINOR', oldValue: 'CRITICAL' }], - webhookSource: 'GitHub', - externalUser: 'toto@github.com', - }, - ], - }); - const wrapper = shallowRender(); - await waitAndUpdate(wrapper); - expect(wrapper).toMatchSnapshot(); -}); - -it('should render the changelog popup when change was triggered by a webhook without user', async () => { - (getIssueChangelog as jest.Mock).mockResolvedValueOnce({ - changelog: [ - { - creationDate: '2017-03-01T09:36:01+0100', - user: null, - isUserActive: false, - diffs: [{ key: 'severity', newValue: 'MINOR', oldValue: 'CRITICAL' }], - webhookSource: 'GitHub', - }, - ], - }); - const wrapper = shallowRender(); - await waitAndUpdate(wrapper); - expect(wrapper).toMatchSnapshot(); -}); - -it('should render the changelog popup with SQ user when both SQ and external user are presents', async () => { - (getIssueChangelog as jest.Mock).mockResolvedValueOnce({ - changelog: [ - { - creationDate: '2017-03-01T09:36:01+0100', - user: 'toto@sonarqube.com', - isUserActive: false, - diffs: [{ key: 'severity', newValue: 'MINOR', oldValue: 'CRITICAL' }], - webhookSource: 'GitHub', - externalUser: 'toto@github.com', - }, - ], - }); - const wrapper = shallowRender(); - await waitAndUpdate(wrapper); - expect(wrapper).toMatchSnapshot(); -}); - -function shallowRender(props: Partial = {}) { - return shallow( - - ); -} diff --git a/server/sonar-web/src/main/js/components/issue/popups/__tests__/CommentDeletePopup-test.tsx b/server/sonar-web/src/main/js/components/issue/popups/__tests__/CommentDeletePopup-test.tsx deleted file mode 100644 index 99d02c25576..00000000000 --- a/server/sonar-web/src/main/js/components/issue/popups/__tests__/CommentDeletePopup-test.tsx +++ /dev/null @@ -1,31 +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 { click } from '../../../../helpers/testUtils'; -import CommentDeletePopup from '../CommentDeletePopup'; - -it('should render the comment delete popup correctly', () => { - const onDelete = jest.fn(); - const element = shallow(); - expect(element).toMatchSnapshot(); - click(element.find('Button')); - expect(onDelete.mock.calls.length).toBe(1); -}); diff --git a/server/sonar-web/src/main/js/components/issue/popups/__tests__/CommentPopup-test.tsx b/server/sonar-web/src/main/js/components/issue/popups/__tests__/CommentPopup-test.tsx deleted file mode 100644 index 669cdfcab98..00000000000 --- a/server/sonar-web/src/main/js/components/issue/popups/__tests__/CommentPopup-test.tsx +++ /dev/null @@ -1,50 +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 { screen } from '@testing-library/react'; -import userEvent from '@testing-library/user-event'; -import * as React from 'react'; -import { renderComponent } from '../../../../helpers/testReactTestingUtils'; -import CommentPopup, { CommentPopupProps } from '../CommentPopup'; - -it('should trigger comment change', async () => { - const user = userEvent.setup(); - const onComment = jest.fn(); - const toggleComment = jest.fn(); - shallowRender({ onComment, toggleComment }); - - expect(await screen.findByRole('textbox')).toHaveFocus(); - await user.keyboard('test'); - await user.keyboard('{Control>}{Enter}{/Control}'); - expect(onComment).toHaveBeenCalledWith('test'); - - await user.click(screen.getByRole('button', { name: 'issue.comment.add_comment.cancel' })); - expect(toggleComment).toHaveBeenCalledWith(false); -}); - -function shallowRender(overrides: Partial = {}) { - return renderComponent( - - ); -} diff --git a/server/sonar-web/src/main/js/components/issue/popups/__tests__/SetAssigneePopup-test.tsx b/server/sonar-web/src/main/js/components/issue/popups/__tests__/SetAssigneePopup-test.tsx deleted file mode 100644 index d225be2f92a..00000000000 --- a/server/sonar-web/src/main/js/components/issue/popups/__tests__/SetAssigneePopup-test.tsx +++ /dev/null @@ -1,52 +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 { searchUsers } from '../../../../api/users'; -import { mockLoggedInUser, mockUser } from '../../../../helpers/testMocks'; -import { waitAndUpdate } from '../../../../helpers/testUtils'; -import { SetAssigneePopup } from '../SetAssigneePopup'; - -jest.mock('../../../../api/users', () => { - const { mockUser } = jest.requireActual('../../../../helpers/testMocks'); - return { searchUsers: jest.fn().mockResolvedValue({ users: [mockUser()] }) }; -}); - -beforeEach(() => { - jest.clearAllMocks(); -}); - -it('should render correctly', () => { - expect(shallowRender()).toMatchSnapshot(); -}); - -it('should allow to search for a user on SQ', async () => { - const wrapper = shallowRender(); - wrapper.find('SearchBox').prop('onChange')('o'); - await waitAndUpdate(wrapper); - expect(searchUsers).toHaveBeenCalledWith({ q: 'o', ps: 10 }); - expect(wrapper.state('users')).toEqual([mockUser()]); -}); - -function shallowRender(props: Partial = {}) { - return shallow( - - ); -} diff --git a/server/sonar-web/src/main/js/components/issue/popups/__tests__/SimilarIssuesPopup-test.tsx b/server/sonar-web/src/main/js/components/issue/popups/__tests__/SimilarIssuesPopup-test.tsx deleted file mode 100644 index e3d89c9d082..00000000000 --- a/server/sonar-web/src/main/js/components/issue/popups/__tests__/SimilarIssuesPopup-test.tsx +++ /dev/null @@ -1,59 +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 { mockIssue } from '../../../../helpers/testMocks'; -import SimilarIssuesPopup from '../SimilarIssuesPopup'; - -it('should render correctly', () => { - expect(shallowRender()).toMatchSnapshot(); -}); - -it('should render correctly when assigned', () => { - expect( - shallowRender({ - issue: mockIssue(false, { assignee: 'luke', assigneeName: 'Luke Skywalker' }), - }).find('SelectListItem[item="assignee"]') - ).toMatchSnapshot(); - - expect( - shallowRender({ issue: mockIssue(false, { assignee: 'luke', assigneeActive: false }) }).find( - 'SelectListItem[item="assignee"]' - ) - ).toMatchSnapshot(); -}); - -it('should filter properly', () => { - const issue = mockIssue(); - const onFilter = jest.fn(); - const wrapper = shallowRender({ issue, onFilter }); - wrapper.find('SelectList').prop('onSelect')('assignee'); - expect(onFilter).toHaveBeenCalledWith('assignee', issue); -}); - -function shallowRender(props: Partial = {}) { - return shallow( - - ); -} diff --git a/server/sonar-web/src/main/js/components/issue/popups/__tests__/__snapshots__/ChangelogPopup-test.tsx.snap b/server/sonar-web/src/main/js/components/issue/popups/__tests__/__snapshots__/ChangelogPopup-test.tsx.snap deleted file mode 100644 index e294878f479..00000000000 --- a/server/sonar-web/src/main/js/components/issue/popups/__tests__/__snapshots__/ChangelogPopup-test.tsx.snap +++ /dev/null @@ -1,319 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`should render the changelog popup correctly 1`] = ` - -
    - - - - - - - - - - - -
    - - - created_by john.david.dalton@gmail.com -
    - - -

    - - John Doe -

    - -
    -
    -
    -`; - -exports[`should render the changelog popup when change was triggered by a webhook with external user 1`] = ` - -
    - - - - - - - - - - - -
    - - - created_by john.david.dalton@gmail.com -
    - - -

    - - toto@github.com - issue.changelog.webhook_source.GitHub -

    - -
    -
    -
    -`; - -exports[`should render the changelog popup when change was triggered by a webhook without user 1`] = ` - -
    - - - - - - - - - - - -
    - - - created_by john.david.dalton@gmail.com -
    - - -

    - issue.changelog.webhook_source.GitHub -

    - -
    -
    -
    -`; - -exports[`should render the changelog popup when we have a deleted user 1`] = ` - -
    - - - - - - - - - - - -
    - - - created_by john.david.dalton@gmail.com -
    - - -

    - - user.x_deleted.john.doe -

    - -
    -
    -
    -`; - -exports[`should render the changelog popup with SQ user when both SQ and external user are presents 1`] = ` - -
    - - - - - - - - - - - -
    - - - created_by john.david.dalton@gmail.com -
    - - -

    - - toto@sonarqube.com - issue.changelog.webhook_source.GitHub -

    - -
    -
    -
    -`; diff --git a/server/sonar-web/src/main/js/components/issue/popups/__tests__/__snapshots__/CommentDeletePopup-test.tsx.snap b/server/sonar-web/src/main/js/components/issue/popups/__tests__/__snapshots__/CommentDeletePopup-test.tsx.snap deleted file mode 100644 index 955955a8e5a..00000000000 --- a/server/sonar-web/src/main/js/components/issue/popups/__tests__/__snapshots__/CommentDeletePopup-test.tsx.snap +++ /dev/null @@ -1,23 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`should render the comment delete popup correctly 1`] = ` - -
    -
    - issue.comment.delete_confirm_message -
    - -
    -
    -`; diff --git a/server/sonar-web/src/main/js/components/issue/popups/__tests__/__snapshots__/SetAssigneePopup-test.tsx.snap b/server/sonar-web/src/main/js/components/issue/popups/__tests__/__snapshots__/SetAssigneePopup-test.tsx.snap deleted file mode 100644 index ae5a826874a..00000000000 --- a/server/sonar-web/src/main/js/components/issue/popups/__tests__/__snapshots__/SetAssigneePopup-test.tsx.snap +++ /dev/null @@ -1,70 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`should render correctly 1`] = ` - -
    -
    - -
    - - - - - Skywalker - - - - - unassigned - - - -
    -
    -`; diff --git a/server/sonar-web/src/main/js/components/issue/popups/__tests__/__snapshots__/SimilarIssuesPopup-test.tsx.snap b/server/sonar-web/src/main/js/components/issue/popups/__tests__/__snapshots__/SimilarIssuesPopup-test.tsx.snap deleted file mode 100644 index 67756efde7f..00000000000 --- a/server/sonar-web/src/main/js/components/issue/popups/__tests__/__snapshots__/SimilarIssuesPopup-test.tsx.snap +++ /dev/null @@ -1,144 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`should render correctly 1`] = ` - -
    -
    - issue.filter_similar_issues -
    -
    - - - - issue.type.BUG - - - - - - - - - unresolved - - - unassigned - -
  • - - foo - - - - - test-tag - - -
  • - - - Foo - - - - main.js - - - -`; - -exports[`should render correctly when assigned 1`] = ` - - - assigned_to - - Luke Skywalker - - -`; - -exports[`should render correctly when assigned 2`] = ` - - - assigned_to - - user.x_deleted.luke - - -`; diff --git a/server/sonar-web/src/main/js/helpers/constants.ts b/server/sonar-web/src/main/js/helpers/constants.ts index 2c7957083cd..a28b90793e2 100644 --- a/server/sonar-web/src/main/js/helpers/constants.ts +++ b/server/sonar-web/src/main/js/helpers/constants.ts @@ -20,10 +20,10 @@ import { colors } from '../app/theme'; import { AlmKeys } from '../types/alm-settings'; import { ComponentQualifier } from '../types/component'; -import { IssueScope, IssueType } from '../types/issues'; +import { IssueScope, IssueSeverity, IssueType } from '../types/issues'; import { RuleType } from '../types/types'; -export const SEVERITIES = ['BLOCKER', 'CRITICAL', 'MAJOR', 'MINOR', 'INFO']; +export const SEVERITIES = Object.values(IssueSeverity); export const STATUSES = ['OPEN', 'REOPENED', 'CONFIRMED', 'RESOLVED', 'CLOSED']; export const ISSUE_TYPES: IssueType[] = [ IssueType.Bug, diff --git a/server/sonar-web/src/main/js/helpers/mocks/issues.ts b/server/sonar-web/src/main/js/helpers/mocks/issues.ts index e77d65a4038..7eb14844271 100644 --- a/server/sonar-web/src/main/js/helpers/mocks/issues.ts +++ b/server/sonar-web/src/main/js/helpers/mocks/issues.ts @@ -17,9 +17,10 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +import { uniqueId } from 'lodash'; import { Query } from '../../apps/issues/utils'; import { ReferencedRule } from '../../types/issues'; -import { IssueChangelog } from '../../types/types'; +import { IssueChangelog, IssueChangelogDiff, IssueComment } from '../../types/types'; export function mockReferencedRule(overrides: Partial = {}): ReferencedRule { return { @@ -35,13 +36,32 @@ export function mockIssueChangelog(overrides: Partial = {}): Iss isUserActive: true, user: 'luke.skywalker', userName: 'Luke Skywalker', - diffs: [ - { - key: 'assign', - newValue: 'darth.vader', - oldValue: 'luke.skywalker', - }, - ], + diffs: [mockIssueChangelogDiff()], + ...overrides, + }; +} + +export function mockIssueChangelogDiff( + overrides: Partial = {} +): IssueChangelogDiff { + return { + key: 'assign', + newValue: 'darth.vader', + oldValue: 'luke.skywalker', + ...overrides, + }; +} + +export function mockIssueComment(overrides: Partial = {}): IssueComment { + return { + author: 'luke.skywalker', + authorLogin: 'luke.skywalker', + authorName: 'Luke Skywalker', + createdAt: '2018-10-01', + htmlText: 'This is a comment, bud', + key: uniqueId(), + markdown: 'This is a *comment*, `bud`', + updatable: false, ...overrides, }; } diff --git a/server/sonar-web/src/main/js/helpers/testMocks.ts b/server/sonar-web/src/main/js/helpers/testMocks.ts index b2824755591..40521eaeac6 100644 --- a/server/sonar-web/src/main/js/helpers/testMocks.ts +++ b/server/sonar-web/src/main/js/helpers/testMocks.ts @@ -295,6 +295,7 @@ export function mockRawIssue(withLocations = false, overrides: Partial status: IssueStatus.Open, textRange: { startLine: 25, endLine: 26, startOffset: 0, endOffset: 15 }, type: IssueType.CodeSmell, + transitions: [], scope: IssueScope.Main, ...overrides, }; diff --git a/server/sonar-web/src/main/js/types/issues.ts b/server/sonar-web/src/main/js/types/issues.ts index c5dea846e56..3e96f1cf3c7 100644 --- a/server/sonar-web/src/main/js/types/issues.ts +++ b/server/sonar-web/src/main/js/types/issues.ts @@ -29,12 +29,13 @@ export enum IssueType { SecurityHotspot = 'SECURITY_HOTSPOT', } +// Keep this enum in the correct order (most severe to least severe). export enum IssueSeverity { Blocker = 'BLOCKER', - Minor = 'MINOR', Critical = 'CRITICAL', - Info = 'INFO', Major = 'MAJOR', + Minor = 'MINOR', + Info = 'INFO', } export enum IssueScope { @@ -58,6 +59,23 @@ export enum IssueStatus { Closed = 'CLOSED', } +export enum IssueActions { + SetType = 'set_type', + SetTags = 'set_tags', + SetSeverity = 'set_severity', + Comment = 'comment', + Assign = 'assign', +} + +export enum IssueTransition { + Confirm = 'confirm', + UnConfirm = 'unconfirm', + Resolve = 'resolve', + FalsePositive = 'falsepositive', + WontFix = 'wontfix', + Reopen = 'reopen', +} + interface Comment { createdAt: string; htmlText: string; @@ -87,11 +105,11 @@ export interface RawFlowLocation { export interface RawIssue { actions: string[]; - transitions?: string[]; + transitions: string[]; tags?: string[]; assignee?: string; author?: string; - comments?: Array; + comments?: Comment[]; creationDate: string; component: string; flows?: Array<{ @@ -120,7 +138,7 @@ export interface IssueResponse { components?: Array<{ key: string; name: string }>; issue: RawIssue; rules?: Array<{}>; - users?: Array; + users?: UserBase[]; } export interface RawIssuesResponse { @@ -131,7 +149,7 @@ export interface RawIssuesResponse { languages: ReferencedLanguage[]; paging: Paging; rules?: Array<{}>; - users?: Array; + users?: UserBase[]; } export interface FetchIssuesPromise { diff --git a/sonar-core/src/main/resources/org/sonar/l10n/core.properties b/sonar-core/src/main/resources/org/sonar/l10n/core.properties index 3b8586ad21b..e40fb501af7 100644 --- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties +++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties @@ -846,6 +846,7 @@ issue.assign.unassigned_click_to_assign=Unassigned, click to assign issue issue.assign.formlink=Assign issue.assign.to_me=to me issue.quick_fix_available_with_sonarlint=Quick fix available in {link} +issue.quick_fix_available_with_sonarlint_no_link=Quick fix available in SonarLint issue.comment.add_comment=Add Comment issue.comment.add_comment.cancel=Cancel adding comment issue.comment.enter_comment=Enter Comment @@ -969,6 +970,7 @@ issues.not_all_issue_show_why=You do not have access to all projects in this por # ISSUE CHANGELOG # #------------------------------------------------------------------------------ +issue.changelog.found_on_x_show_more=Found on {0}; click to see changelog issue.changelog.changed_to={0} changed to {1} issue.changelog.was=was {0} issue.changelog.webhook_source= (change triggered by a {0} webhook)