Browse Source

SONAR-13086 Improve activity and comment layout for Security Hotspots

tags/9.1.0.47736
Wouter Admiraal 2 years ago
parent
commit
8da77e6255
17 changed files with 1136 additions and 682 deletions
  1. 4
    4
      server/sonar-web/src/main/js/apps/security-hotspots/__tests__/utils-test.ts
  2. 115
    96
      server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotReviewHistory.tsx
  3. 45
    65
      server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotReviewHistoryAndComments.tsx
  4. 6
    23
      server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotViewer.tsx
  5. 3
    9
      server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotViewerRenderer.tsx
  6. 135
    40
      server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotReviewHistory-test.tsx
  7. 13
    27
      server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotReviewHistoryAndComments-test.tsx
  8. 2
    10
      server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotViewer-test.tsx
  9. 1
    3
      server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotViewerRenderer-test.tsx
  10. 598
    186
      server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotReviewHistory-test.tsx.snap
  11. 178
    193
      server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotReviewHistoryAndComments-test.tsx.snap
  12. 2
    6
      server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotViewer-test.tsx.snap
  13. 0
    18
      server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotViewerRenderer-test.tsx.snap
  14. 1
    1
      server/sonar-web/src/main/js/apps/security-hotspots/utils.ts
  15. 17
    0
      server/sonar-web/src/main/js/helpers/mocks/issues.ts
  16. 14
    0
      server/sonar-web/src/main/js/helpers/mocks/security-hotspots.ts
  17. 2
    1
      sonar-core/src/main/resources/org/sonar/l10n/core.properties

+ 4
- 4
server/sonar-web/src/main/js/apps/security-hotspots/__tests__/utils-test.ts View File

@@ -191,14 +191,14 @@ describe('getHotspotReviewHistory', () => {
const reviewHistory = getHotspotReviewHistory(hotspot);

expect(reviewHistory.length).toBe(4);
expect(reviewHistory[0]).toEqual(
expect(reviewHistory[3]).toEqual(
expect.objectContaining({
type: ReviewHistoryType.Creation,
date: hotspot.creationDate,
user: hotspot.authorUser
})
);
expect(reviewHistory[1]).toEqual(
expect(reviewHistory[2]).toEqual(
expect.objectContaining({
type: ReviewHistoryType.Comment,
date: commentElement.createdAt,
@@ -206,7 +206,7 @@ describe('getHotspotReviewHistory', () => {
html: commentElement.htmlText
})
);
expect(reviewHistory[2]).toEqual(
expect(reviewHistory[1]).toEqual(
expect.objectContaining({
type: ReviewHistoryType.Comment,
date: commentElement1.createdAt,
@@ -214,7 +214,7 @@ describe('getHotspotReviewHistory', () => {
html: commentElement1.htmlText
})
);
expect(reviewHistory[3]).toEqual(
expect(reviewHistory[0]).toEqual(
expect.objectContaining({
type: ReviewHistoryType.Diff,
date: changelogElement.creationDate,

+ 115
- 96
server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotReviewHistory.tsx View File

@@ -19,7 +19,7 @@
*/
import * as classNames from 'classnames';
import * as React from 'react';
import { Button, DeleteButton, EditButton } from '../../../components/controls/buttons';
import { Button, ButtonLink, DeleteButton, EditButton } from '../../../components/controls/buttons';
import Dropdown, { DropdownOverlay } from '../../../components/controls/Dropdown';
import Toggler from '../../../components/controls/Toggler';
import DateTimeFormatter from '../../../components/intl/DateTimeFormatter';
@@ -36,112 +36,131 @@ export interface HotspotReviewHistoryProps {
hotspot: Hotspot;
onDeleteComment: (key: string) => void;
onEditComment: (key: string, comment: string) => void;
onShowFullHistory: () => void;
showFullHistory: boolean;
}

export const MAX_RECENT_ACTIVITY = 5;

export default function HotspotReviewHistory(props: HotspotReviewHistoryProps) {
const { hotspot } = props;
const reviewHistory = getHotspotReviewHistory(hotspot);
const { hotspot, showFullHistory } = props;
const fullReviewHistory = getHotspotReviewHistory(hotspot);
const [editedCommentKey, setEditedCommentKey] = React.useState('');

const reviewHistory = showFullHistory
? fullReviewHistory
: fullReviewHistory.slice(0, MAX_RECENT_ACTIVITY);

return (
<>
{reviewHistory.map((historyElt, historyIndex) => {
const { user, type, diffs, date, html, key, updatable, markdown } = historyElt;
return (
<div
className={classNames('padded', { 'bordered-top': historyIndex > 0 })}
key={historyIndex}>
<div className="display-flex-center">
{user.name && (
<>
<Avatar
className="little-spacer-right"
hash={user.avatar}
name={user.name}
size={20}
/>
<strong>
{user.active ? user.name : translateWithParameters('user.x_deleted', user.name)}
</strong>
{type === ReviewHistoryType.Creation && (
<span className="little-spacer-left">
{translate('hotspots.review_history.created')}
</span>
)}
{type === ReviewHistoryType.Comment && (
<span className="little-spacer-left">
{translate('hotspots.review_history.comment_added')}
</span>
)}
<span className="little-spacer-left little-spacer-right">-</span>
</>
)}
<DateTimeFormatter date={date} />
</div>

{type === ReviewHistoryType.Diff && diffs && (
<div className="spacer-top">
{diffs.map((diff, diffIndex) => (
<IssueChangelogDiff diff={diff} key={diffIndex} />
))}
<ul>
{reviewHistory.map((historyElt, historyIndex) => {
const { user, type, diffs, date, html, key, updatable, markdown } = historyElt;
return (
<li
className={classNames('padded-top padded-bottom', {
'bordered-top': historyIndex > 0
})}
key={historyIndex}>
<div className="display-flex-center">
{user.name && (
<>
<Avatar
className="little-spacer-right"
hash={user.avatar}
name={user.name}
size={20}
/>
<strong>
{user.active
? user.name
: translateWithParameters('user.x_deleted', user.name)}
</strong>
{type === ReviewHistoryType.Creation && (
<span className="little-spacer-left">
{translate('hotspots.review_history.created')}
</span>
)}
{type === ReviewHistoryType.Comment && (
<span className="little-spacer-left">
{translate('hotspots.review_history.comment_added')}
</span>
)}
<span className="little-spacer-left little-spacer-right">-</span>
</>
)}
<DateTimeFormatter date={date} />
</div>
)}

{type === ReviewHistoryType.Comment && key && html && markdown && (
<div className="spacer-top display-flex-space-between">
<div
className="markdown"
// eslint-disable-next-line react/no-danger
dangerouslySetInnerHTML={{ __html: sanitizeString(html) }}
/>
{updatable && (
<div>
<div className="dropdown">
<Toggler
onRequestClose={() => {
setEditedCommentKey('');
}}
open={key === editedCommentKey}
{type === ReviewHistoryType.Diff && diffs && (
<div className="spacer-top">
{diffs.map((diff, diffIndex) => (
<IssueChangelogDiff diff={diff} key={diffIndex} />
))}
</div>
)}

{type === ReviewHistoryType.Comment && key && html && markdown && (
<div className="spacer-top display-flex-space-between">
<div
className="markdown"
// eslint-disable-next-line react/no-danger
dangerouslySetInnerHTML={{ __html: sanitizeString(html) }}
/>
{updatable && (
<div>
<div className="dropdown">
<Toggler
onRequestClose={() => {
setEditedCommentKey('');
}}
open={key === editedCommentKey}
overlay={
<DropdownOverlay placement={PopupPlacement.BottomRight}>
<HotspotCommentPopup
markdownComment={markdown}
onCancelEdit={() => setEditedCommentKey('')}
onCommentEditSubmit={comment => {
setEditedCommentKey('');
props.onEditComment(key, comment);
}}
/>
</DropdownOverlay>
}>
<EditButton
className="button-small"
onClick={() => setEditedCommentKey(key)}
/>
</Toggler>
</div>
<Dropdown
onOpen={() => setEditedCommentKey('')}
overlay={
<DropdownOverlay placement={PopupPlacement.BottomRight}>
<HotspotCommentPopup
markdownComment={markdown}
onCancelEdit={() => setEditedCommentKey('')}
onCommentEditSubmit={comment => {
setEditedCommentKey('');
props.onEditComment(key, comment);
}}
/>
</DropdownOverlay>
}>
<EditButton
className="button-small"
onClick={() => setEditedCommentKey(key)}
/>
</Toggler>
<div className="padded abs-width-150">
<p>{translate('issue.comment.delete_confirm_message')}</p>
<Button
className="button-red big-spacer-top pull-right"
onClick={() => props.onDeleteComment(key)}>
{translate('delete')}
</Button>
</div>
}
overlayPlacement={PopupPlacement.BottomRight}>
<DeleteButton className="button-small" />
</Dropdown>
</div>
<Dropdown
onOpen={() => setEditedCommentKey('')}
overlay={
<div className="padded abs-width-150">
<p>{translate('issue.comment.delete_confirm_message')}</p>
<Button
className="button-red big-spacer-top pull-right"
onClick={() => props.onDeleteComment(key)}>
{translate('delete')}
</Button>
</div>
}
overlayPlacement={PopupPlacement.BottomRight}>
<DeleteButton className="button-small" />
</Dropdown>
</div>
)}
</div>
)}
</div>
);
})}
)}
</div>
)}
</li>
);
})}
</ul>
{!showFullHistory && fullReviewHistory.length > MAX_RECENT_ACTIVITY && (
<ButtonLink className="spacer-top" onClick={props.onShowFullHistory}>
{translate('show_all')}
</ButtonLink>
)}
</>
);
}

+ 45
- 65
server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotReviewHistoryAndComments.tsx View File

@@ -17,7 +17,6 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as classNames from 'classnames';
import * as React from 'react';
import {
commentSecurityHotspot,
@@ -25,7 +24,7 @@ import {
editSecurityHotspotComment
} from '../../../api/security-hotspots';
import FormattingTips from '../../../components/common/FormattingTips';
import { Button, ResetButtonLink } from '../../../components/controls/buttons';
import { Button } from '../../../components/controls/buttons';
import { translate } from '../../../helpers/l10n';
import { isLoggedIn } from '../../../helpers/users';
import { Hotspot } from '../../../types/security-hotspots';
@@ -35,28 +34,28 @@ interface Props {
currentUser: T.CurrentUser;
hotspot: Hotspot;
commentTextRef: React.RefObject<HTMLTextAreaElement>;
commentVisible: boolean;
onCommentUpdate: () => void;
onOpenComment: () => void;
onCloseComment: () => void;
}

interface State {
comment: string;
showFullHistory: boolean;
}

export default class HotspotReviewHistoryAndComments extends React.PureComponent<Props, State> {
constructor(props: Props) {
super(props);
this.state = {
comment: ''
comment: '',
showFullHistory: false
};
}

componentDidUpdate(prevProps: Props) {
if (prevProps.hotspot !== this.props.hotspot) {
if (prevProps.hotspot.key !== this.props.hotspot.key) {
this.setState({
comment: ''
comment: '',
showFullHistory: false
});
}
}
@@ -65,15 +64,9 @@ export default class HotspotReviewHistoryAndComments extends React.PureComponent
this.setState({ comment: event.target.value });
};

handleCloseComment = () => {
this.setState({ comment: '' });
this.props.onCloseComment();
};

handleSubmitComment = () => {
return commentSecurityHotspot(this.props.hotspot.key, this.state.comment).then(() => {
this.setState({ comment: '' });
this.props.onCloseComment();
this.props.onCommentUpdate();
});
};
@@ -90,62 +83,49 @@ export default class HotspotReviewHistoryAndComments extends React.PureComponent
});
};

handleShowFullHistory = () => {
this.setState({ showFullHistory: true });
};

render() {
const { currentUser, hotspot, commentTextRef, commentVisible } = this.props;
const { comment } = this.state;
const { currentUser, hotspot, commentTextRef } = this.props;
const { comment, showFullHistory } = this.state;
return (
<>
<h1>{translate('hotspot.section.activity')}</h1>
<div className="padded it__hs-review-history">
<HotspotReviewHistory
hotspot={hotspot}
onDeleteComment={this.handleDeleteComment}
onEditComment={this.handleEditComment}
/>

{isLoggedIn(currentUser) && (
<>
<hr />
<div className="big-spacer-top">
<div className="padded it__hs-review-history">
{isLoggedIn(currentUser) && (
<div className="big-spacer-top">
<div className="little-spacer-bottom">{translate('hotspots.comment.field')}</div>
<textarea
className="form-field fixed-width width-100 spacer-bottom"
onChange={this.handleCommentChange}
ref={commentTextRef}
rows={2}
value={comment}
/>
<div className="display-flex-space-between display-flex-center ">
<FormattingTips className="huge-spacer-bottom" />
<div>
<Button
className={classNames('it__hs-add-comment', { invisible: commentVisible })}
id="hotspot-comment-box-display"
onClick={this.props.onOpenComment}>
{translate('hotspots.comment.open')}
className="huge-spacer-bottom"
id="hotspot-comment-box-submit"
onClick={this.handleSubmitComment}>
{translate('hotspots.comment.submit')}
</Button>

<div className={classNames({ invisible: !commentVisible })}>
<div className="little-spacer-bottom">{translate('hotspots.comment.field')}</div>
<textarea
className="form-field fixed-width width-100 spacer-bottom"
onChange={this.handleCommentChange}
ref={commentTextRef}
rows={2}
value={comment}
/>
<div className="display-flex-space-between display-flex-center ">
<FormattingTips className="huge-spacer-bottom" />
<div>
<Button
className="huge-spacer-bottom"
id="hotspot-comment-box-submit"
onClick={this.handleSubmitComment}>
{translate('hotspots.comment.submit')}
</Button>
<ResetButtonLink
className="spacer-left huge-spacer-bottom"
id="hotspot-comment-box-cancel"
onClick={this.handleCloseComment}>
{translate('cancel')}
</ResetButtonLink>
</div>
</div>
</div>
</div>
</>
)}
</div>
</>
</div>
</div>
)}

<h2 className="spacer-top big-spacer-bottom">{translate('hotspot.section.activity')}</h2>

<HotspotReviewHistory
hotspot={hotspot}
onDeleteComment={this.handleDeleteComment}
onEditComment={this.handleEditComment}
onShowFullHistory={this.handleShowFullHistory}
showFullHistory={showFullHistory}
/>
</div>
);
}
}

+ 6
- 23
server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotViewer.tsx View File

@@ -43,7 +43,6 @@ interface State {
hotspot?: Hotspot;
lastStatusChangedTo?: HotspotStatusOption;
loading: boolean;
commentVisible: boolean;
showStatusUpdateSuccessModal: boolean;
}

@@ -55,7 +54,7 @@ export default class HotspotViewer extends React.PureComponent<Props, State> {
constructor(props: Props) {
super(props);
this.commentTextRef = React.createRef<HTMLTextAreaElement>();
this.state = { loading: false, commentVisible: false, showStatusUpdateSuccessModal: false };
this.state = { loading: false, showStatusUpdateSuccessModal: false };
}

componentDidMount() {
@@ -63,11 +62,11 @@ export default class HotspotViewer extends React.PureComponent<Props, State> {
this.fetchHotspot();
}

componentDidUpdate(prevProps: Props, prevState: State) {
componentDidUpdate(prevProps: Props) {
if (prevProps.hotspotKey !== this.props.hotspotKey) {
this.fetchHotspot();
}
if (this.commentTextRef.current && !prevState.commentVisible && this.state.commentVisible) {
if (this.commentTextRef.current) {
this.commentTextRef.current.focus({ preventScroll: true });
}
}
@@ -99,23 +98,15 @@ export default class HotspotViewer extends React.PureComponent<Props, State> {
}
};

handleOpenComment = () => {
this.setState({ commentVisible: true });
handleScrollToCommentForm = () => {
if (this.commentTextRef.current) {
// Edge case when the comment is already open and unfocus.
this.commentTextRef.current.focus({ preventScroll: true });
}
if (this.commentTextRef.current) {
scrollToElement(this.commentTextRef.current, {
bottomOffset: 100
});
}
};

handleCloseComment = () => {
this.setState({ commentVisible: false });
};

handleSwitchFilterToStatusOfUpdatedHotspot = () => {
const { lastStatusChangedTo } = this.state;
if (lastStatusChangedTo) {
@@ -129,28 +120,20 @@ export default class HotspotViewer extends React.PureComponent<Props, State> {

render() {
const { branchLike, component, hotspotsReviewedMeasure, securityCategories } = this.props;
const {
hotspot,
lastStatusChangedTo,
loading,
commentVisible,
showStatusUpdateSuccessModal
} = this.state;
const { hotspot, lastStatusChangedTo, loading, showStatusUpdateSuccessModal } = this.state;

return (
<HotspotViewerRenderer
branchLike={branchLike}
component={component}
commentTextRef={this.commentTextRef}
commentVisible={commentVisible}
hotspot={hotspot}
hotspotsReviewedMeasure={hotspotsReviewedMeasure}
lastStatusChangedTo={lastStatusChangedTo}
loading={loading}
onCloseComment={this.handleCloseComment}
onCloseStatusUpdateSuccessModal={this.handleCloseStatusUpdateSuccessModal}
onOpenComment={this.handleOpenComment}
onSwitchFilterToStatusOfUpdatedHotspot={this.handleSwitchFilterToStatusOfUpdatedHotspot}
onShowCommentForm={this.handleScrollToCommentForm}
onUpdateHotspot={this.handleHotspotUpdate}
showStatusUpdateSuccessModal={showStatusUpdateSuccessModal}
securityCategories={securityCategories}

+ 3
- 9
server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotViewerRenderer.tsx View File

@@ -52,12 +52,10 @@ export interface HotspotViewerRendererProps {
hotspotsReviewedMeasure?: string;
lastStatusChangedTo?: HotspotStatusOption;
loading: boolean;
commentVisible: boolean;
commentTextRef: React.RefObject<HTMLTextAreaElement>;
onOpenComment: () => void;
onCloseComment: () => void;
onCloseStatusUpdateSuccessModal: () => void;
onUpdateHotspot: (statusUpdate?: boolean, statusOption?: HotspotStatusOption) => Promise<void>;
onShowCommentForm: () => void;
onSwitchFilterToStatusOfUpdatedHotspot: () => void;
showStatusUpdateSuccessModal: boolean;
securityCategories: T.StandardSecurityCategories;
@@ -74,8 +72,7 @@ export function HotspotViewerRenderer(props: HotspotViewerRendererProps) {
lastStatusChangedTo,
showStatusUpdateSuccessModal,
securityCategories,
commentTextRef,
commentVisible
commentTextRef
} = props;

const permalink = getPathUrlAsString(
@@ -115,7 +112,7 @@ export function HotspotViewerRenderer(props: HotspotViewerRendererProps) {
{isLoggedIn(currentUser) && (
<>
<div className="dropdown spacer-right flex-1-0-auto">
<Button className="it__hs-add-comment" onClick={props.onOpenComment}>
<Button className="it__hs-add-comment" onClick={props.onShowCommentForm}>
{translate('hotspots.comment.open')}
</Button>
</div>
@@ -175,12 +172,9 @@ export function HotspotViewerRenderer(props: HotspotViewerRendererProps) {
<HotspotViewerTabs hotspot={hotspot} />
<HotspotReviewHistoryAndComments
commentTextRef={commentTextRef}
commentVisible={commentVisible}
currentUser={currentUser}
hotspot={hotspot}
onCloseComment={props.onCloseComment}
onCommentUpdate={props.onUpdateHotspot}
onOpenComment={props.onOpenComment}
/>
</div>
)}

+ 135
- 40
server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotReviewHistory-test.tsx View File

@@ -19,57 +19,152 @@
*/
import { shallow } from 'enzyme';
import * as React from 'react';
import { mockHotspot } from '../../../../helpers/mocks/security-hotspots';
import { Button, EditButton } from '../../../../components/controls/buttons';
import Dropdown, { DropdownOverlay } from '../../../../components/controls/Dropdown';
import Toggler from '../../../../components/controls/Toggler';
import { mockIssueChangelog } from '../../../../helpers/mocks/issues';
import { mockHotspot, mockHotspotComment } from '../../../../helpers/mocks/security-hotspots';
import { mockUser } from '../../../../helpers/testMocks';
import HotspotCommentPopup from '../HotspotCommentPopup';
import HotspotReviewHistory, { HotspotReviewHistoryProps } from '../HotspotReviewHistory';

it('should render correctly', () => {
const wrapper = shallowRender();
expect(wrapper).toMatchSnapshot();
expect(shallowRender()).toMatchSnapshot('default');
expect(shallowRender({ showFullHistory: true })).toMatchSnapshot('show full list');
expect(
shallowRender({ showFullHistory: true })
.find(Toggler)
.props().overlay
).toMatchSnapshot('edit comment overlay');
expect(
shallowRender({ showFullHistory: true })
.find(Dropdown)
.props().overlay
).toMatchSnapshot('delete comment overlay');
});

function shallowRender(props?: Partial<HotspotReviewHistoryProps>) {
const changelogElement: T.IssueChangelog = {
creationDate: '2018-10-01',
isUserActive: true,
user: 'me',
userName: 'me-name',
diffs: [
{
key: 'assign',
newValue: 'me',
oldValue: 'him'
}
]
};
const commentElement = {
key: 'comment-1',
createdAt: '2018-09-10',
htmlText: '<strong>TEST</strong>',
markdown: '*TEST*',
updatable: true,
login: 'dude-1',
user: mockUser({ login: 'dude-1' })
};
const commentElement1 = {
key: 'comment-2',
createdAt: '2018-09-11',
htmlText: '<strong>TEST</strong>',
markdown: '*TEST*',
updatable: false,
login: 'dude-2',
user: mockUser({ login: 'dude-2' })
};
const hotspot = mockHotspot({
creationDate: '2018-09-01',
changelog: [changelogElement],
comment: [commentElement, commentElement1]
it('should correctly handle comment updating', () => {
return new Promise<void>((resolve, reject) => {
const setEditedCommentKey = jest.fn();
jest.spyOn(React, 'useState').mockImplementationOnce(() => ['', setEditedCommentKey]);

const onEditComment = jest.fn();
const wrapper = shallowRender({ onEditComment, showFullHistory: true });

// Closing the Toggler sets the edited key back to an empty string.
wrapper
.find(Toggler)
.at(0)
.props()
.onRequestClose();
expect(setEditedCommentKey).toBeCalledWith('');

const editOnClick = wrapper
.find(EditButton)
.at(0)
.props().onClick;
if (!editOnClick) {
reject();
return;
}

// Clicking on the EditButton correctly flags the comment for editing.
editOnClick();
expect(setEditedCommentKey).toHaveBeenLastCalledWith('comment-1');

// Cancelling an edit sets the edited key back to an empty string
const dropdownOverlay = shallow(
wrapper
.find(Toggler)
.at(0)
.props().overlay as React.ReactElement<DropdownOverlay>
);
dropdownOverlay
.find(HotspotCommentPopup)
.props()
.onCancelEdit();
expect(setEditedCommentKey).toHaveBeenLastCalledWith('');

// Updating the comment sets the edited key back to an empty string, and calls the
// prop to update the comment value.
dropdownOverlay
.find(HotspotCommentPopup)
.props()
.onCommentEditSubmit('comment');
expect(onEditComment).toHaveBeenLastCalledWith('comment-1', 'comment');
expect(setEditedCommentKey).toHaveBeenLastCalledWith('');
expect(setEditedCommentKey).toHaveBeenCalledTimes(4);

resolve();
});
});

it('should correctly handle comment deleting', () => {
return new Promise<void>((resolve, reject) => {
const setEditedCommentKey = jest.fn();
jest.spyOn(React, 'useState').mockImplementationOnce(() => ['', setEditedCommentKey]);

const onDeleteComment = jest.fn();
const wrapper = shallowRender({ onDeleteComment, showFullHistory: true });

// Opening the deletion Dropdown sets the edited key back to an empty string.
const dropdownOnOpen = wrapper
.find(Dropdown)
.at(0)
.props().onOpen;
if (!dropdownOnOpen) {
reject();
return;
}
dropdownOnOpen();
expect(setEditedCommentKey).toHaveBeenLastCalledWith('');

// Confirming deletion calls the prop to delete the comment.
const dropdownOverlay = shallow(
wrapper
.find(Dropdown)
.at(0)
.props().overlay as React.ReactElement<HTMLDivElement>
);
const deleteButtonOnClick = dropdownOverlay.find(Button).props().onClick;
if (!deleteButtonOnClick) {
reject();
return;
}

deleteButtonOnClick();
expect(onDeleteComment).toBeCalledWith('comment-1');

resolve();
});
});

function shallowRender(props?: Partial<HotspotReviewHistoryProps>) {
return shallow(
<HotspotReviewHistory
hotspot={hotspot}
hotspot={mockHotspot({
creationDate: '2018-09-01',
changelog: [
mockIssueChangelog(),
mockIssueChangelog({
creationDate: '2018-10-12'
})
],
comment: [
mockHotspotComment({
key: 'comment-1',
updatable: true
}),
mockHotspotComment({ key: 'comment-2', user: mockUser({ name: undefined }) }),
mockHotspotComment({ key: 'comment-3', user: mockUser({ active: false }) }),
mockHotspotComment({ key: 'comment-4' }),
mockHotspotComment({ key: 'comment-5' })
]
})}
onDeleteComment={jest.fn()}
onEditComment={jest.fn()}
onShowFullHistory={jest.fn()}
showFullHistory={false}
{...props}
/>
);

+ 13
- 27
server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotReviewHistoryAndComments-test.tsx View File

@@ -50,16 +50,10 @@ it('should render correctly without user', () => {
expect(wrapper).toMatchSnapshot();
});

it('should open comment form', () => {
const wrapper = shallowRender();
wrapper.find('#hotspot-comment-box-display').simulate('click');
expect(wrapper.instance().props.onOpenComment).toHaveBeenCalled();
});

it('should submit comment', async () => {
const mockApi = commentSecurityHotspot as jest.Mock;
const hotspot = mockHotspot();
const wrapper = shallowRender({ hotspot, commentVisible: true });
const wrapper = shallowRender({ hotspot });
mockApi.mockClear();
wrapper.instance().setState({ comment: 'Comment' });

@@ -68,26 +62,11 @@ it('should submit comment', async () => {

expect(mockApi).toHaveBeenCalledWith(hotspot.key, 'Comment');
expect(wrapper.state().comment).toBe('');
expect(wrapper.instance().props.onCloseComment).toHaveBeenCalledTimes(1);
expect(wrapper.instance().props.onCommentUpdate).toHaveBeenCalledTimes(1);
});

it('should cancel comment', () => {
const mockApi = commentSecurityHotspot as jest.Mock;
const hotspot = mockHotspot();
const wrapper = shallowRender({ hotspot, commentVisible: true });
wrapper.instance().setState({ comment: 'Comment' });
mockApi.mockClear();

wrapper.find('#hotspot-comment-box-cancel').simulate('click');

expect(mockApi).not.toHaveBeenCalled();
expect(wrapper.state().comment).toBe('');
expect(wrapper.instance().props.onCloseComment).toHaveBeenCalledTimes(1);
});

it('should change comment', () => {
const wrapper = shallowRender({ commentVisible: true });
const wrapper = shallowRender();
wrapper.instance().setState({ comment: 'Comment' });
wrapper.find('textarea').simulate('change', { target: { value: 'Foo' } });

@@ -97,7 +76,7 @@ it('should change comment', () => {
it('should reset on change hotspot', () => {
const wrapper = shallowRender();
wrapper.setState({ comment: 'NOP' });
wrapper.setProps({ hotspot: mockHotspot() });
wrapper.setProps({ hotspot: mockHotspot({ key: 'other-hotspot' }) });

expect(wrapper.state().comment).toBe('');
});
@@ -122,16 +101,23 @@ it('should edit comment', async () => {
expect(wrapper.instance().props.onCommentUpdate).toBeCalledTimes(1);
});

it('should correctly toggle the show full history state', () => {
const wrapper = shallowRender();
expect(wrapper.state().showFullHistory).toBe(false);
wrapper
.find(HotspotReviewHistory)
.props()
.onShowFullHistory();
expect(wrapper.state().showFullHistory).toBe(true);
});

function shallowRender(props?: Partial<HotspotReviewHistoryAndComments['props']>) {
return shallow<HotspotReviewHistoryAndComments>(
<HotspotReviewHistoryAndComments
commentTextRef={React.createRef()}
commentVisible={false}
currentUser={mockCurrentUser()}
hotspot={mockHotspot()}
onCloseComment={jest.fn()}
onCommentUpdate={jest.fn()}
onOpenComment={jest.fn()}
{...props}
/>
);

+ 2
- 10
server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotViewer-test.tsx View File

@@ -104,27 +104,19 @@ it('should NOT refresh hotspot list on assignee/comment updates', () => {
expect(onUpdateHotspot).not.toHaveBeenCalled();
});

it('should open comment form when scroll to comment', () => {
it('should scroll to comment form', () => {
const wrapper = shallowRender();
const mockTextRef = ({ current: { focus: jest.fn() } } as any) as React.RefObject<
HTMLTextAreaElement
>;
wrapper.instance().commentTextRef = mockTextRef;

wrapper.find(HotspotViewerRenderer).simulate('openComment');
wrapper.find(HotspotViewerRenderer).simulate('showCommentForm');

expect(wrapper.state().commentVisible).toBe(true);
expect(mockTextRef.current?.focus).toHaveBeenCalled();
expect(scrollToElement).toHaveBeenCalledWith(mockTextRef.current, expect.anything());
});

it('should close comment', () => {
const wrapper = shallowRender();
wrapper.setState({ commentVisible: true });
wrapper.find(HotspotViewerRenderer).simulate('closeComment');
expect(wrapper.state().commentVisible).toBe(false);
});

it('should reset loading even on fetch error', async () => {
const mockGetHostpot = getSecurityHotspotDetails as jest.Mock;
mockGetHostpot.mockRejectedValueOnce({});

+ 1
- 3
server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotViewerRenderer-test.tsx View File

@@ -69,16 +69,14 @@ function shallowRender(props?: Partial<HotspotViewerRendererProps>) {
branchLike={mockBranch()}
component={mockComponent()}
commentTextRef={React.createRef()}
commentVisible={false}
currentUser={mockCurrentUser()}
hotspot={mockHotspot()}
hotspotsReviewedMeasure="75"
lastStatusChangedTo={HotspotStatusOption.FIXED}
loading={false}
onCloseComment={jest.fn()}
onCloseStatusUpdateSuccessModal={jest.fn()}
onOpenComment={jest.fn()}
onSwitchFilterToStatusOfUpdatedHotspot={jest.fn()}
onShowCommentForm={jest.fn()}
onUpdateHotspot={jest.fn()}
securityCategories={{ 'sql-injection': { title: 'SQL injection' } }}
showStatusUpdateSuccessModal={false}

+ 598
- 186
server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotReviewHistory-test.tsx.snap View File

@@ -1,208 +1,620 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`should render correctly 1`] = `
exports[`should render correctly: default 1`] = `
<Fragment>
<div
className="padded"
key="0"
>
<div
className="display-flex-center"
<ul>
<li
className="padded-top padded-bottom"
key="0"
>
<Connect(Avatar)
className="little-spacer-right"
name="John Doe"
size={20}
/>
<strong>
John Doe
</strong>
<span
className="little-spacer-left"
>
hotspots.review_history.created
</span>
<span
className="little-spacer-left little-spacer-right"
>
-
</span>
<DateTimeFormatter
date="2018-09-01"
/>
</div>
</div>
<div
className="padded bordered-top"
key="1"
>
<div
className="display-flex-center"
<div
className="display-flex-center"
>
<Connect(Avatar)
className="little-spacer-right"
name="Luke Skywalker"
size={20}
/>
<strong>
Luke Skywalker
</strong>
<span
className="little-spacer-left little-spacer-right"
>
-
</span>
<DateTimeFormatter
date="2018-10-12"
/>
</div>
<div
className="spacer-top"
>
<IssueChangelogDiff
diff={
Object {
"key": "assign",
"newValue": "darth.vader",
"oldValue": "luke.skywalker",
}
}
key="0"
/>
</div>
</li>
<li
className="padded-top padded-bottom bordered-top"
key="1"
>
<Connect(Avatar)
className="little-spacer-right"
name="John Doe"
size={20}
/>
<strong>
John Doe
</strong>
<span
className="little-spacer-left"
>
hotspots.review_history.comment_added
</span>
<span
className="little-spacer-left little-spacer-right"
>
-
</span>
<DateTimeFormatter
date="2018-09-10"
/>
</div>
<div
className="spacer-top display-flex-space-between"
<div
className="display-flex-center"
>
<Connect(Avatar)
className="little-spacer-right"
name="Luke Skywalker"
size={20}
/>
<strong>
Luke Skywalker
</strong>
<span
className="little-spacer-left little-spacer-right"
>
-
</span>
<DateTimeFormatter
date="2018-10-01"
/>
</div>
<div
className="spacer-top"
>
<IssueChangelogDiff
diff={
Object {
"key": "assign",
"newValue": "darth.vader",
"oldValue": "luke.skywalker",
}
}
key="0"
/>
</div>
</li>
<li
className="padded-top padded-bottom bordered-top"
key="2"
>
<div
className="markdown"
dangerouslySetInnerHTML={
Object {
"__html": "<strong>TEST</strong>",
className="display-flex-center"
>
<Connect(Avatar)
className="little-spacer-right"
name="John Doe"
size={20}
/>
<strong>
John Doe
</strong>
<span
className="little-spacer-left"
>
hotspots.review_history.comment_added
</span>
<span
className="little-spacer-left little-spacer-right"
>
-
</span>
<DateTimeFormatter
date="2018-09-10"
/>
</div>
<div
className="spacer-top display-flex-space-between"
>
<div
className="markdown"
dangerouslySetInnerHTML={
Object {
"__html": "<strong>TEST</strong>",
}
}
}
/>
<div>
/>
</div>
</li>
<li
className="padded-top padded-bottom bordered-top"
key="3"
>
<div
className="display-flex-center"
>
<Connect(Avatar)
className="little-spacer-right"
name="John Doe"
size={20}
/>
<strong>
John Doe
</strong>
<span
className="little-spacer-left"
>
hotspots.review_history.comment_added
</span>
<span
className="little-spacer-left little-spacer-right"
>
-
</span>
<DateTimeFormatter
date="2018-09-10"
/>
</div>
<div
className="spacer-top display-flex-space-between"
>
<div
className="dropdown"
className="markdown"
dangerouslySetInnerHTML={
Object {
"__html": "<strong>TEST</strong>",
}
}
/>
</div>
</li>
<li
className="padded-top padded-bottom bordered-top"
key="4"
>
<div
className="display-flex-center"
>
<Connect(Avatar)
className="little-spacer-right"
name="John Doe"
size={20}
/>
<strong>
user.x_deleted.John Doe
</strong>
<span
className="little-spacer-left"
>
<Toggler
onRequestClose={[Function]}
open={false}
overlay={
<DropdownOverlay
placement="bottom-right"
>
<HotspotCommentPopup
markdownComment="*TEST*"
onCancelEdit={[Function]}
onCommentEditSubmit={[Function]}
/>
</DropdownOverlay>
hotspots.review_history.comment_added
</span>
<span
className="little-spacer-left little-spacer-right"
>
-
</span>
<DateTimeFormatter
date="2018-09-10"
/>
</div>
<div
className="spacer-top display-flex-space-between"
>
<div
className="markdown"
dangerouslySetInnerHTML={
Object {
"__html": "<strong>TEST</strong>",
}
>
<EditButton
className="button-small"
onClick={[Function]}
/>
</Toggler>
</div>
<Dropdown
onOpen={[Function]}
overlay={
<div
className="padded abs-width-150"
>
<p>
issue.comment.delete_confirm_message
</p>
<Button
className="button-red big-spacer-top pull-right"
onClick={[Function]}
>
delete
</Button>
</div>
}
overlayPlacement="bottom-right"
>
<DeleteButton
className="button-small"
/>
</Dropdown>
</div>
</div>
</div>
<div
className="padded bordered-top"
key="2"
/>
</div>
</li>
</ul>
<ButtonLink
className="spacer-top"
onClick={[MockFunction]}
>
<div
className="display-flex-center"
show_all
</ButtonLink>
</Fragment>
`;

exports[`should render correctly: delete comment overlay 1`] = `
<div
className="padded abs-width-150"
>
<p>
issue.comment.delete_confirm_message
</p>
<Button
className="button-red big-spacer-top pull-right"
onClick={[Function]}
>
delete
</Button>
</div>
`;

exports[`should render correctly: edit comment overlay 1`] = `
<DropdownOverlay
placement="bottom-right"
>
<HotspotCommentPopup
markdownComment="*TEST*"
onCancelEdit={[Function]}
onCommentEditSubmit={[Function]}
/>
</DropdownOverlay>
`;

exports[`should render correctly: show full list 1`] = `
<Fragment>
<ul>
<li
className="padded-top padded-bottom"
key="0"
>
<Connect(Avatar)
className="little-spacer-right"
name="John Doe"
size={20}
/>
<strong>
John Doe
</strong>
<span
className="little-spacer-left"
>
hotspots.review_history.comment_added
</span>
<span
className="little-spacer-left little-spacer-right"
>
-
</span>
<DateTimeFormatter
date="2018-09-11"
/>
</div>
<div
className="spacer-top display-flex-space-between"
<div
className="display-flex-center"
>
<Connect(Avatar)
className="little-spacer-right"
name="Luke Skywalker"
size={20}
/>
<strong>
Luke Skywalker
</strong>
<span
className="little-spacer-left little-spacer-right"
>
-
</span>
<DateTimeFormatter
date="2018-10-12"
/>
</div>
<div
className="spacer-top"
>
<IssueChangelogDiff
diff={
Object {
"key": "assign",
"newValue": "darth.vader",
"oldValue": "luke.skywalker",
}
}
key="0"
/>
</div>
</li>
<li
className="padded-top padded-bottom bordered-top"
key="1"
>
<div
className="markdown"
dangerouslySetInnerHTML={
Object {
"__html": "<strong>TEST</strong>",
className="display-flex-center"
>
<Connect(Avatar)
className="little-spacer-right"
name="Luke Skywalker"
size={20}
/>
<strong>
Luke Skywalker
</strong>
<span
className="little-spacer-left little-spacer-right"
>
-
</span>
<DateTimeFormatter
date="2018-10-01"
/>
</div>
<div
className="spacer-top"
>
<IssueChangelogDiff
diff={
Object {
"key": "assign",
"newValue": "darth.vader",
"oldValue": "luke.skywalker",
}
}
}
/>
</div>
</div>
<div
className="padded bordered-top"
key="3"
>
<div
className="display-flex-center"
key="0"
/>
</div>
</li>
<li
className="padded-top padded-bottom bordered-top"
key="2"
>
<div
className="display-flex-center"
>
<Connect(Avatar)
className="little-spacer-right"
name="John Doe"
size={20}
/>
<strong>
John Doe
</strong>
<span
className="little-spacer-left"
>
hotspots.review_history.comment_added
</span>
<span
className="little-spacer-left little-spacer-right"
>
-
</span>
<DateTimeFormatter
date="2018-09-10"
/>
</div>
<div
className="spacer-top display-flex-space-between"
>
<div
className="markdown"
dangerouslySetInnerHTML={
Object {
"__html": "<strong>TEST</strong>",
}
}
/>
</div>
</li>
<li
className="padded-top padded-bottom bordered-top"
key="3"
>
<div
className="display-flex-center"
>
<Connect(Avatar)
className="little-spacer-right"
name="John Doe"
size={20}
/>
<strong>
John Doe
</strong>
<span
className="little-spacer-left"
>
hotspots.review_history.comment_added
</span>
<span
className="little-spacer-left little-spacer-right"
>
-
</span>
<DateTimeFormatter
date="2018-09-10"
/>
</div>
<div
className="spacer-top display-flex-space-between"
>
<div
className="markdown"
dangerouslySetInnerHTML={
Object {
"__html": "<strong>TEST</strong>",
}
}
/>
</div>
</li>
<li
className="padded-top padded-bottom bordered-top"
key="4"
>
<div
className="display-flex-center"
>
<Connect(Avatar)
className="little-spacer-right"
name="John Doe"
size={20}
/>
<strong>
user.x_deleted.John Doe
</strong>
<span
className="little-spacer-left"
>
hotspots.review_history.comment_added
</span>
<span
className="little-spacer-left little-spacer-right"
>
-
</span>
<DateTimeFormatter
date="2018-09-10"
/>
</div>
<div
className="spacer-top display-flex-space-between"
>
<div
className="markdown"
dangerouslySetInnerHTML={
Object {
"__html": "<strong>TEST</strong>",
}
}
/>
</div>
</li>
<li
className="padded-top padded-bottom bordered-top"
key="5"
>
<Connect(Avatar)
className="little-spacer-right"
name="me-name"
size={20}
/>
<strong>
me-name
</strong>
<span
className="little-spacer-left little-spacer-right"
>
-
</span>
<DateTimeFormatter
date="2018-10-01"
/>
</div>
<div
className="spacer-top"
<div
className="display-flex-center"
>
<Connect(Avatar)
className="little-spacer-right"
name="john.doe"
size={20}
/>
<strong>
john.doe
</strong>
<span
className="little-spacer-left"
>
hotspots.review_history.comment_added
</span>
<span
className="little-spacer-left little-spacer-right"
>
-
</span>
<DateTimeFormatter
date="2018-09-10"
/>
</div>
<div
className="spacer-top display-flex-space-between"
>
<div
className="markdown"
dangerouslySetInnerHTML={
Object {
"__html": "<strong>TEST</strong>",
}
}
/>
</div>
</li>
<li
className="padded-top padded-bottom bordered-top"
key="6"
>
<IssueChangelogDiff
diff={
Object {
"key": "assign",
"newValue": "me",
"oldValue": "him",
<div
className="display-flex-center"
>
<Connect(Avatar)
className="little-spacer-right"
name="John Doe"
size={20}
/>
<strong>
John Doe
</strong>
<span
className="little-spacer-left"
>
hotspots.review_history.comment_added
</span>
<span
className="little-spacer-left little-spacer-right"
>
-
</span>
<DateTimeFormatter
date="2018-09-10"
/>
</div>
<div
className="spacer-top display-flex-space-between"
>
<div
className="markdown"
dangerouslySetInnerHTML={
Object {
"__html": "<strong>TEST</strong>",
}
}
}
key="0"
/>
</div>
</div>
/>
<div>
<div
className="dropdown"
>
<Toggler
onRequestClose={[Function]}
open={false}
overlay={
<DropdownOverlay
placement="bottom-right"
>
<HotspotCommentPopup
markdownComment="*TEST*"
onCancelEdit={[Function]}
onCommentEditSubmit={[Function]}
/>
</DropdownOverlay>
}
>
<EditButton
className="button-small"
onClick={[Function]}
/>
</Toggler>
</div>
<Dropdown
onOpen={[Function]}
overlay={
<div
className="padded abs-width-150"
>
<p>
issue.comment.delete_confirm_message
</p>
<Button
className="button-red big-spacer-top pull-right"
onClick={[Function]}
>
delete
</Button>
</div>
}
overlayPlacement="bottom-right"
>
<DeleteButton
className="button-small"
/>
</Dropdown>
</div>
</div>
</li>
<li
className="padded-top padded-bottom bordered-top"
key="7"
>
<div
className="display-flex-center"
>
<Connect(Avatar)
className="little-spacer-right"
name="John Doe"
size={20}
/>
<strong>
John Doe
</strong>
<span
className="little-spacer-left"
>
hotspots.review_history.created
</span>
<span
className="little-spacer-left little-spacer-right"
>
-
</span>
<DateTimeFormatter
date="2018-09-01"
/>
</div>
</li>
</ul>
</Fragment>
`;

+ 178
- 193
server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotReviewHistoryAndComments-test.tsx.snap View File

@@ -1,225 +1,210 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`should render correctly 1`] = `
<Fragment>
<h1>
hotspot.section.activity
</h1>
<div
className="padded it__hs-review-history"
>
<div
className="padded it__hs-review-history"
className="big-spacer-top"
>
<HotspotReviewHistory
hotspot={
Object {
"assignee": "assignee",
"assigneeUser": Object {
<div
className="little-spacer-bottom"
>
hotspots.comment.field
</div>
<textarea
className="form-field fixed-width width-100 spacer-bottom"
onChange={[Function]}
rows={2}
value=""
/>
<div
className="display-flex-space-between display-flex-center "
>
<FormattingTips
className="huge-spacer-bottom"
/>
<div>
<Button
className="huge-spacer-bottom"
id="hotspot-comment-box-submit"
onClick={[Function]}
>
hotspots.comment.submit
</Button>
</div>
</div>
</div>
<h2
className="spacer-top big-spacer-bottom"
>
hotspot.section.activity
</h2>
<HotspotReviewHistory
hotspot={
Object {
"assignee": "assignee",
"assigneeUser": Object {
"active": true,
"local": true,
"login": "assignee",
"name": "John Doe",
},
"author": "author",
"authorUser": Object {
"active": true,
"local": true,
"login": "author",
"name": "John Doe",
},
"canChangeStatus": true,
"changelog": Array [],
"comment": Array [],
"component": Object {
"key": "hotspot-component",
"longName": "Hotspot component long name",
"name": "Hotspot Component",
"path": "path/to/component",
"qualifier": "FIL",
},
"creationDate": "2013-05-13T17:55:41+0200",
"key": "01fc972e-2a3c-433e-bcae-0bd7f88f5123",
"line": 142,
"message": "'3' is a magic number.",
"project": Object {
"key": "hotspot-component",
"longName": "Hotspot component long name",
"name": "Hotspot Component",
"path": "path/to/component",
"qualifier": "TRK",
},
"resolution": "FIXED",
"rule": Object {
"fixRecommendations": "<p>This a <strong>strong</strong> message about fixing !</p>",
"key": "squid:S2077",
"name": "That rule",
"riskDescription": "<p>This a <strong>strong</strong> message about risk !</p>",
"securityCategory": "sql-injection",
"vulnerabilityDescription": "<p>This a <strong>strong</strong> message about vulnerability !</p>",
"vulnerabilityProbability": "HIGH",
},
"status": "REVIEWED",
"textRange": Object {
"endLine": 142,
"endOffset": 83,
"startLine": 142,
"startOffset": 26,
},
"updateDate": "2013-05-13T17:55:42+0200",
"users": Array [
Object {
"active": true,
"local": true,
"login": "assignee",
"name": "John Doe",
},
"author": "author",
"authorUser": Object {
Object {
"active": true,
"local": true,
"login": "author",
"name": "John Doe",
},
"canChangeStatus": true,
"changelog": Array [],
"comment": Array [],
"component": Object {
"key": "hotspot-component",
"longName": "Hotspot component long name",
"name": "Hotspot Component",
"path": "path/to/component",
"qualifier": "FIL",
},
"creationDate": "2013-05-13T17:55:41+0200",
"key": "01fc972e-2a3c-433e-bcae-0bd7f88f5123",
"line": 142,
"message": "'3' is a magic number.",
"project": Object {
"key": "hotspot-component",
"longName": "Hotspot component long name",
"name": "Hotspot Component",
"path": "path/to/component",
"qualifier": "TRK",
},
"resolution": "FIXED",
"rule": Object {
"fixRecommendations": "<p>This a <strong>strong</strong> message about fixing !</p>",
"key": "squid:S2077",
"name": "That rule",
"riskDescription": "<p>This a <strong>strong</strong> message about risk !</p>",
"securityCategory": "sql-injection",
"vulnerabilityDescription": "<p>This a <strong>strong</strong> message about vulnerability !</p>",
"vulnerabilityProbability": "HIGH",
},
"status": "REVIEWED",
"textRange": Object {
"endLine": 142,
"endOffset": 83,
"startLine": 142,
"startOffset": 26,
},
"updateDate": "2013-05-13T17:55:42+0200",
"users": Array [
Object {
"active": true,
"local": true,
"login": "assignee",
"name": "John Doe",
},
Object {
"active": true,
"local": true,
"login": "author",
"name": "John Doe",
},
],
}
],
}
onDeleteComment={[Function]}
onEditComment={[Function]}
/>
<hr />
<div
className="big-spacer-top"
>
<Button
className="it__hs-add-comment"
id="hotspot-comment-box-display"
onClick={[MockFunction]}
>
hotspots.comment.open
</Button>
<div
className="invisible"
>
<div
className="little-spacer-bottom"
>
hotspots.comment.field
</div>
<textarea
className="form-field fixed-width width-100 spacer-bottom"
onChange={[Function]}
rows={2}
value=""
/>
<div
className="display-flex-space-between display-flex-center "
>
<FormattingTips
className="huge-spacer-bottom"
/>
<div>
<Button
className="huge-spacer-bottom"
id="hotspot-comment-box-submit"
onClick={[Function]}
>
hotspots.comment.submit
</Button>
<ResetButtonLink
className="spacer-left huge-spacer-bottom"
id="hotspot-comment-box-cancel"
onClick={[Function]}
>
cancel
</ResetButtonLink>
</div>
</div>
</div>
</div>
</div>
</Fragment>
}
onDeleteComment={[Function]}
onEditComment={[Function]}
onShowFullHistory={[Function]}
showFullHistory={false}
/>
</div>
`;

exports[`should render correctly without user 1`] = `
<Fragment>
<h1>
hotspot.section.activity
</h1>
<div
className="padded it__hs-review-history"
<div
className="padded it__hs-review-history"
>
<h2
className="spacer-top big-spacer-bottom"
>
<HotspotReviewHistory
hotspot={
Object {
"assignee": "assignee",
"assigneeUser": Object {
hotspot.section.activity
</h2>
<HotspotReviewHistory
hotspot={
Object {
"assignee": "assignee",
"assigneeUser": Object {
"active": true,
"local": true,
"login": "assignee",
"name": "John Doe",
},
"author": "author",
"authorUser": Object {
"active": true,
"local": true,
"login": "author",
"name": "John Doe",
},
"canChangeStatus": true,
"changelog": Array [],
"comment": Array [],
"component": Object {
"key": "hotspot-component",
"longName": "Hotspot component long name",
"name": "Hotspot Component",
"path": "path/to/component",
"qualifier": "FIL",
},
"creationDate": "2013-05-13T17:55:41+0200",
"key": "01fc972e-2a3c-433e-bcae-0bd7f88f5123",
"line": 142,
"message": "'3' is a magic number.",
"project": Object {
"key": "hotspot-component",
"longName": "Hotspot component long name",
"name": "Hotspot Component",
"path": "path/to/component",
"qualifier": "TRK",
},
"resolution": "FIXED",
"rule": Object {
"fixRecommendations": "<p>This a <strong>strong</strong> message about fixing !</p>",
"key": "squid:S2077",
"name": "That rule",
"riskDescription": "<p>This a <strong>strong</strong> message about risk !</p>",
"securityCategory": "sql-injection",
"vulnerabilityDescription": "<p>This a <strong>strong</strong> message about vulnerability !</p>",
"vulnerabilityProbability": "HIGH",
},
"status": "REVIEWED",
"textRange": Object {
"endLine": 142,
"endOffset": 83,
"startLine": 142,
"startOffset": 26,
},
"updateDate": "2013-05-13T17:55:42+0200",
"users": Array [
Object {
"active": true,
"local": true,
"login": "assignee",
"name": "John Doe",
},
"author": "author",
"authorUser": Object {
Object {
"active": true,
"local": true,
"login": "author",
"name": "John Doe",
},
"canChangeStatus": true,
"changelog": Array [],
"comment": Array [],
"component": Object {
"key": "hotspot-component",
"longName": "Hotspot component long name",
"name": "Hotspot Component",
"path": "path/to/component",
"qualifier": "FIL",
},
"creationDate": "2013-05-13T17:55:41+0200",
"key": "01fc972e-2a3c-433e-bcae-0bd7f88f5123",
"line": 142,
"message": "'3' is a magic number.",
"project": Object {
"key": "hotspot-component",
"longName": "Hotspot component long name",
"name": "Hotspot Component",
"path": "path/to/component",
"qualifier": "TRK",
},
"resolution": "FIXED",
"rule": Object {
"fixRecommendations": "<p>This a <strong>strong</strong> message about fixing !</p>",
"key": "squid:S2077",
"name": "That rule",
"riskDescription": "<p>This a <strong>strong</strong> message about risk !</p>",
"securityCategory": "sql-injection",
"vulnerabilityDescription": "<p>This a <strong>strong</strong> message about vulnerability !</p>",
"vulnerabilityProbability": "HIGH",
},
"status": "REVIEWED",
"textRange": Object {
"endLine": 142,
"endOffset": 83,
"startLine": 142,
"startOffset": 26,
},
"updateDate": "2013-05-13T17:55:42+0200",
"users": Array [
Object {
"active": true,
"local": true,
"login": "assignee",
"name": "John Doe",
},
Object {
"active": true,
"local": true,
"login": "author",
"name": "John Doe",
},
],
}
],
}
onDeleteComment={[Function]}
onEditComment={[Function]}
/>
</div>
</Fragment>
}
onDeleteComment={[Function]}
onEditComment={[Function]}
onShowFullHistory={[Function]}
showFullHistory={false}
/>
</div>
`;

+ 2
- 6
server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotViewer-test.tsx.snap View File

@@ -7,7 +7,6 @@ exports[`should render correctly 1`] = `
"current": null,
}
}
commentVisible={false}
component={
Object {
"breadcrumbs": Array [],
@@ -31,9 +30,8 @@ exports[`should render correctly 1`] = `
}
}
loading={true}
onCloseComment={[Function]}
onCloseStatusUpdateSuccessModal={[Function]}
onOpenComment={[Function]}
onShowCommentForm={[Function]}
onSwitchFilterToStatusOfUpdatedHotspot={[Function]}
onUpdateHotspot={[Function]}
securityCategories={
@@ -54,7 +52,6 @@ exports[`should render correctly 2`] = `
"current": null,
}
}
commentVisible={false}
component={
Object {
"breadcrumbs": Array [],
@@ -83,9 +80,8 @@ exports[`should render correctly 2`] = `
}
}
loading={false}
onCloseComment={[Function]}
onCloseStatusUpdateSuccessModal={[Function]}
onOpenComment={[Function]}
onShowCommentForm={[Function]}
onSwitchFilterToStatusOfUpdatedHotspot={[Function]}
onUpdateHotspot={[Function]}
securityCategories={

+ 0
- 18
server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotViewerRenderer-test.tsx.snap View File

@@ -451,7 +451,6 @@ exports[`should render correctly: anonymous user 1`] = `
"current": null,
}
}
commentVisible={false}
currentUser={
Object {
"isLoggedIn": false,
@@ -528,9 +527,7 @@ exports[`should render correctly: anonymous user 1`] = `
],
}
}
onCloseComment={[MockFunction]}
onCommentUpdate={[MockFunction]}
onOpenComment={[MockFunction]}
/>
</div>
</DeferredSpinner>
@@ -987,7 +984,6 @@ exports[`should render correctly: assignee without name 1`] = `
"current": null,
}
}
commentVisible={false}
currentUser={
Object {
"isLoggedIn": false,
@@ -1064,9 +1060,7 @@ exports[`should render correctly: assignee without name 1`] = `
],
}
}
onCloseComment={[MockFunction]}
onCommentUpdate={[MockFunction]}
onOpenComment={[MockFunction]}
/>
</div>
</DeferredSpinner>
@@ -1523,7 +1517,6 @@ exports[`should render correctly: default 1`] = `
"current": null,
}
}
commentVisible={false}
currentUser={
Object {
"isLoggedIn": false,
@@ -1600,9 +1593,7 @@ exports[`should render correctly: default 1`] = `
],
}
}
onCloseComment={[MockFunction]}
onCommentUpdate={[MockFunction]}
onOpenComment={[MockFunction]}
/>
</div>
</DeferredSpinner>
@@ -2059,7 +2050,6 @@ exports[`should render correctly: deleted assignee 1`] = `
"current": null,
}
}
commentVisible={false}
currentUser={
Object {
"isLoggedIn": false,
@@ -2136,9 +2126,7 @@ exports[`should render correctly: deleted assignee 1`] = `
],
}
}
onCloseComment={[MockFunction]}
onCommentUpdate={[MockFunction]}
onOpenComment={[MockFunction]}
/>
</div>
</DeferredSpinner>
@@ -2608,7 +2596,6 @@ exports[`should render correctly: show success modal 1`] = `
"current": null,
}
}
commentVisible={false}
currentUser={
Object {
"isLoggedIn": false,
@@ -2685,9 +2672,7 @@ exports[`should render correctly: show success modal 1`] = `
],
}
}
onCloseComment={[MockFunction]}
onCommentUpdate={[MockFunction]}
onOpenComment={[MockFunction]}
/>
</div>
</DeferredSpinner>
@@ -3144,7 +3129,6 @@ exports[`should render correctly: unassigned 1`] = `
"current": null,
}
}
commentVisible={false}
currentUser={
Object {
"isLoggedIn": false,
@@ -3221,9 +3205,7 @@ exports[`should render correctly: unassigned 1`] = `
],
}
}
onCloseComment={[MockFunction]}
onCommentUpdate={[MockFunction]}
onOpenComment={[MockFunction]}
/>
</div>
</DeferredSpinner>

+ 1
- 1
server/sonar-web/src/main/js/apps/security-hotspots/utils.ts View File

@@ -148,7 +148,7 @@ export function getHotspotReviewHistory(hotspot: Hotspot): ReviewHistoryElement[
);
}

return sortBy(history, elt => elt.date);
return sortBy(history, elt => elt.date).reverse();
}

const STATUS_AND_RESOLUTION_TO_STATUS_OPTION = {

+ 17
- 0
server/sonar-web/src/main/js/helpers/mocks/issues.ts View File

@@ -26,3 +26,20 @@ export function mockReferencedRule(overrides: Partial<ReferencedRule> = {}): Ref
...overrides
};
}

export function mockIssueChangelog(overrides: Partial<T.IssueChangelog> = {}): T.IssueChangelog {
return {
creationDate: '2018-10-01',
isUserActive: true,
user: 'luke.skywalker',
userName: 'Luke Skywalker',
diffs: [
{
key: 'assign',
newValue: 'darth.vader',
oldValue: 'luke.skywalker'
}
],
...overrides
};
}

+ 14
- 0
server/sonar-web/src/main/js/helpers/mocks/security-hotspots.ts View File

@@ -21,6 +21,7 @@ import { ComponentQualifier } from '../../types/component';
import { Standards } from '../../types/security';
import {
Hotspot,
HotspotComment,
HotspotComponent,
HotspotResolution,
HotspotRule,
@@ -94,6 +95,19 @@ export function mockHotspotComponent(overrides?: Partial<HotspotComponent>): Hot
};
}

export function mockHotspotComment(overrides?: Partial<HotspotComment>): HotspotComment {
return {
key: 'comment-1',
createdAt: '2018-09-10',
htmlText: '<strong>TEST</strong>',
markdown: '*TEST*',
updatable: false,
login: 'dude-2',
user: mockUser({ login: 'dude-2' }),
...overrides
};
}

export function mockHotspotRule(overrides?: Partial<HotspotRule>): HotspotRule {
return {
key: 'squid:S2077',

+ 2
- 1
sonar-core/src/main/resources/org/sonar/l10n/core.properties View File

@@ -184,6 +184,7 @@ set=Set
set_up=Set Up
severity=Severity
shared=Shared
show_all=Show all
start_date=Start Date
x_show={0} shown
x_selected={0} selected
@@ -784,7 +785,7 @@ hotspot.filters.period.since_leak_period=New code
hotspot.filters.period.overall=Overall code
hotspot.filters.status.safe=Reviewed as safe
hotspot.filters.show_all=Show all hotspots
hotspot.section.activity=Activity:
hotspot.section.activity=Recent activity:

hotspots.reviewed.tooltip=Percentage of Security Hotspots reviewed (fixed or safe) among all non-closed Security Hotspots.
hotspots.review_hotspot=Review Hotspot

Loading…
Cancel
Save