aboutsummaryrefslogtreecommitdiffstats
path: root/server/sonar-web/src/main/js/apps/security-hotspots/components/status
diff options
context:
space:
mode:
Diffstat (limited to 'server/sonar-web/src/main/js/apps/security-hotspots/components/status')
-rw-r--r--server/sonar-web/src/main/js/apps/security-hotspots/components/status/Status.css44
-rw-r--r--server/sonar-web/src/main/js/apps/security-hotspots/components/status/Status.tsx107
-rw-r--r--server/sonar-web/src/main/js/apps/security-hotspots/components/status/StatusDescription.tsx41
-rw-r--r--server/sonar-web/src/main/js/apps/security-hotspots/components/status/StatusSelection.tsx109
-rw-r--r--server/sonar-web/src/main/js/apps/security-hotspots/components/status/StatusSelectionRenderer.tsx91
-rw-r--r--server/sonar-web/src/main/js/apps/security-hotspots/components/status/__tests__/Status-test.tsx74
-rw-r--r--server/sonar-web/src/main/js/apps/security-hotspots/components/status/__tests__/StatusDescription-test.tsx35
-rw-r--r--server/sonar-web/src/main/js/apps/security-hotspots/components/status/__tests__/StatusSelection-test.tsx78
-rw-r--r--server/sonar-web/src/main/js/apps/security-hotspots/components/status/__tests__/StatusSelectionRenderer-test.tsx72
-rw-r--r--server/sonar-web/src/main/js/apps/security-hotspots/components/status/__tests__/__snapshots__/Status-test.tsx.snap436
-rw-r--r--server/sonar-web/src/main/js/apps/security-hotspots/components/status/__tests__/__snapshots__/StatusDescription-test.tsx.snap24
-rw-r--r--server/sonar-web/src/main/js/apps/security-hotspots/components/status/__tests__/__snapshots__/StatusSelection-test.tsx.snap12
-rw-r--r--server/sonar-web/src/main/js/apps/security-hotspots/components/status/__tests__/__snapshots__/StatusSelectionRenderer-test.tsx.snap140
13 files changed, 1263 insertions, 0 deletions
diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/status/Status.css b/server/sonar-web/src/main/js/apps/security-hotspots/components/status/Status.css
new file mode 100644
index 00000000000..b48cb0745fc
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/status/Status.css
@@ -0,0 +1,44 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2020 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.
+ */
+
+#status-trigger,
+.popup {
+ width: 400px;
+ box-sizing: border-box;
+}
+
+#status-trigger {
+ height: 80px;
+ border-radius: 4px;
+ outline: none;
+}
+
+#status-trigger.readonly {
+ cursor: not-allowed;
+}
+
+#status-trigger:not(.readonly) {
+ cursor: pointer;
+ background-color: var(--darkBlue);
+}
+
+#status-trigger:not(.readonly) * {
+ color: white;
+}
diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/status/Status.tsx b/server/sonar-web/src/main/js/apps/security-hotspots/components/status/Status.tsx
new file mode 100644
index 00000000000..006b7f58932
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/status/Status.tsx
@@ -0,0 +1,107 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2020 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 * as classNames from 'classnames';
+import * as React from 'react';
+import { DropdownOverlay } from 'sonar-ui-common/components/controls/Dropdown';
+import Toggler from 'sonar-ui-common/components/controls/Toggler';
+import Tooltip from 'sonar-ui-common/components/controls/Tooltip';
+import ChevronDownIcon from 'sonar-ui-common/components/icons/ChevronDownIcon';
+import { PopupPlacement } from 'sonar-ui-common/components/ui/popups';
+import { translate } from 'sonar-ui-common/helpers/l10n';
+import { withCurrentUser } from '../../../../components/hoc/withCurrentUser';
+import { isLoggedIn } from '../../../../helpers/users';
+import { Hotspot } from '../../../../types/security-hotspots';
+import { getStatusOptionFromStatusAndResolution } from '../../utils';
+import './Status.css';
+import StatusDescription from './StatusDescription';
+import StatusSelection from './StatusSelection';
+
+export interface StatusProps {
+ currentUser: T.CurrentUser;
+ hotspot: Hotspot;
+
+ onStatusChange: () => void;
+}
+
+export function Status(props: StatusProps) {
+ const { currentUser, hotspot } = props;
+ const [isOpen, setIsOpen] = React.useState(false);
+
+ const statusOption = getStatusOptionFromStatusAndResolution(hotspot.status, hotspot.resolution);
+ const readonly = !hotspot.canChangeStatus || !isLoggedIn(currentUser);
+
+ const trigger = (
+ <div
+ aria-expanded={isOpen}
+ aria-haspopup={true}
+ className={classNames('padded bordered display-flex-column display-flex-justify-center', {
+ readonly
+ })}
+ id="status-trigger"
+ onClick={() => !readonly && setIsOpen(true)}
+ role="button"
+ tabIndex={0}>
+ <div className="display-flex-center display-flex-space-between">
+ {isOpen ? (
+ <span className="h3">{translate('hotspots.status.select_status')}</span>
+ ) : (
+ <StatusDescription showTitle={true} statusOption={statusOption} />
+ )}
+ {!readonly && <ChevronDownIcon className="big-spacer-left" />}
+ </div>
+ </div>
+ );
+
+ const actionableTrigger = (
+ <Toggler
+ closeOnClickOutside={true}
+ closeOnEscape={true}
+ onRequestClose={() => setIsOpen(false)}
+ open={isOpen}
+ overlay={
+ <DropdownOverlay noPadding={true} placement={PopupPlacement.Bottom}>
+ <StatusSelection
+ hotspot={hotspot}
+ onStatusOptionChange={() => {
+ setIsOpen(false);
+ props.onStatusChange();
+ }}
+ />
+ </DropdownOverlay>
+ }>
+ {trigger}
+ </Toggler>
+ );
+
+ return (
+ <div className="dropdown huge-spacer-left">
+ {readonly ? (
+ <Tooltip overlay={translate('hotspots.status.cannot_change_status')} placement="bottom">
+ {actionableTrigger}
+ </Tooltip>
+ ) : (
+ actionableTrigger
+ )}
+ </div>
+ );
+}
+
+export default withCurrentUser(Status);
diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/status/StatusDescription.tsx b/server/sonar-web/src/main/js/apps/security-hotspots/components/status/StatusDescription.tsx
new file mode 100644
index 00000000000..ae853a786c6
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/status/StatusDescription.tsx
@@ -0,0 +1,41 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2020 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 * as React from 'react';
+import { translate } from 'sonar-ui-common/helpers/l10n';
+import { HotspotStatusOption } from '../../../../types/security-hotspots';
+
+export interface StatusDescriptionProps {
+ statusOption: HotspotStatusOption;
+ showTitle?: boolean;
+}
+
+export default function StatusDescription(props: StatusDescriptionProps) {
+ const { statusOption, showTitle } = props;
+
+ return (
+ <div>
+ <h3>
+ {showTitle && `${translate('status')}: `}
+ {translate('hotspots.status_option', statusOption)}
+ </h3>
+ <span>{translate('hotspots.status_option', statusOption, 'description')}</span>
+ </div>
+ );
+}
diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/status/StatusSelection.tsx b/server/sonar-web/src/main/js/apps/security-hotspots/components/status/StatusSelection.tsx
new file mode 100644
index 00000000000..943fc94f904
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/status/StatusSelection.tsx
@@ -0,0 +1,109 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2020 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 * as React from 'react';
+import { setSecurityHotspotStatus } from '../../../../api/security-hotspots';
+import { Hotspot, HotspotStatusOption } from '../../../../types/security-hotspots';
+import {
+ getStatusAndResolutionFromStatusOption,
+ getStatusOptionFromStatusAndResolution
+} from '../../utils';
+import StatusSelectionRenderer from './StatusSelectionRenderer';
+
+interface Props {
+ hotspot: Hotspot;
+ onStatusOptionChange: (statusOption: HotspotStatusOption) => void;
+}
+
+interface State {
+ comment?: string;
+ loading: boolean;
+ initialStatus: HotspotStatusOption;
+ selectedStatus: HotspotStatusOption;
+}
+
+export default class StatusSelection extends React.PureComponent<Props, State> {
+ mounted = false;
+
+ constructor(props: Props) {
+ super(props);
+
+ const initialStatus = getStatusOptionFromStatusAndResolution(
+ props.hotspot.status,
+ props.hotspot.resolution
+ );
+
+ this.state = {
+ loading: false,
+ initialStatus,
+ selectedStatus: initialStatus
+ };
+ }
+
+ componentDidMount() {
+ this.mounted = true;
+ }
+
+ componentWillUnmount() {
+ this.mounted = false;
+ }
+
+ handleStatusChange = (selectedStatus: HotspotStatusOption) => {
+ this.setState({ selectedStatus });
+ };
+
+ handleCommentChange = (comment: string) => {
+ this.setState({ comment });
+ };
+
+ handleSubmit = () => {
+ const { hotspot } = this.props;
+ const { comment, initialStatus, selectedStatus } = this.state;
+
+ if (selectedStatus && selectedStatus !== initialStatus) {
+ this.setState({ loading: true });
+ setSecurityHotspotStatus(hotspot.key, {
+ ...getStatusAndResolutionFromStatusOption(selectedStatus),
+ comment: comment || undefined
+ })
+ .then(() => {
+ this.setState({ loading: false });
+ this.props.onStatusOptionChange(selectedStatus);
+ })
+ .catch(() => this.setState({ loading: false }));
+ }
+ };
+
+ render() {
+ const { comment, initialStatus, loading, selectedStatus } = this.state;
+ const submitDisabled = selectedStatus === initialStatus;
+
+ return (
+ <StatusSelectionRenderer
+ comment={comment}
+ loading={loading}
+ onCommentChange={this.handleCommentChange}
+ onStatusChange={this.handleStatusChange}
+ onSubmit={this.handleSubmit}
+ selectedStatus={selectedStatus}
+ submitDisabled={submitDisabled}
+ />
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/status/StatusSelectionRenderer.tsx b/server/sonar-web/src/main/js/apps/security-hotspots/components/status/StatusSelectionRenderer.tsx
new file mode 100644
index 00000000000..16487044701
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/status/StatusSelectionRenderer.tsx
@@ -0,0 +1,91 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2020 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 * as React from 'react';
+import { SubmitButton } from 'sonar-ui-common/components/controls/buttons';
+import Radio from 'sonar-ui-common/components/controls/Radio';
+import { translate } from 'sonar-ui-common/helpers/l10n';
+import MarkdownTips from '../../../../components/common/MarkdownTips';
+import { HotspotStatusOption } from '../../../../types/security-hotspots';
+import StatusDescription from './StatusDescription';
+
+export interface StatusSelectionRendererProps {
+ selectedStatus: HotspotStatusOption;
+ onStatusChange: (statusOption: HotspotStatusOption) => void;
+
+ comment?: string;
+ onCommentChange: (comment: string) => void;
+
+ onSubmit: () => void;
+
+ loading: boolean;
+ submitDisabled: boolean;
+}
+
+export default function StatusSelectionRenderer(props: StatusSelectionRendererProps) {
+ const { comment, loading, selectedStatus, submitDisabled } = props;
+
+ const renderOption = (status: HotspotStatusOption) => {
+ return (
+ <Radio
+ checked={selectedStatus === status}
+ className="big-spacer-bottom"
+ onCheck={props.onStatusChange}
+ value={status}>
+ <StatusDescription statusOption={status} />
+ </Radio>
+ );
+ };
+
+ return (
+ <>
+ <div className="big-padded">
+ {renderOption(HotspotStatusOption.TO_REVIEW)}
+ {renderOption(HotspotStatusOption.FIXED)}
+ {renderOption(HotspotStatusOption.SAFE)}
+ </div>
+
+ <hr />
+ <div className="big-padded display-flex-column">
+ <label className="text-bold" htmlFor="comment-textarea">
+ {translate('hotspots.status.add_comment')}
+ </label>
+ <textarea
+ className="spacer-top form-field fixed-width spacer-bottom"
+ id="comment-textarea"
+ onChange={(event: React.ChangeEvent<HTMLTextAreaElement>) =>
+ props.onCommentChange(event.currentTarget.value)
+ }
+ rows={4}
+ value={comment}
+ />
+ <MarkdownTips />
+
+ <div className="big-spacer-top display-flex-justify-end display-flex-center">
+ <SubmitButton disabled={submitDisabled || loading} onClick={props.onSubmit}>
+ {translate('hotspots.status.change_status')}
+ </SubmitButton>
+
+ {loading && <i className="spacer-left spinner" />}
+ </div>
+ </div>
+ </>
+ );
+}
diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/status/__tests__/Status-test.tsx b/server/sonar-web/src/main/js/apps/security-hotspots/components/status/__tests__/Status-test.tsx
new file mode 100644
index 00000000000..309dba42348
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/status/__tests__/Status-test.tsx
@@ -0,0 +1,74 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2020 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 { DropdownOverlay } from 'sonar-ui-common/components/controls/Dropdown';
+import Toggler from 'sonar-ui-common/components/controls/Toggler';
+import { click } from 'sonar-ui-common/helpers/testUtils';
+import { mockHotspot } from '../../../../../helpers/mocks/security-hotspots';
+import { mockCurrentUser } from '../../../../../helpers/testMocks';
+import { HotspotStatusOption } from '../../../../../types/security-hotspots';
+import { Status, StatusProps } from '../Status';
+import StatusSelection from '../StatusSelection';
+
+it('should render correctly', () => {
+ const wrapper = shallowRender();
+ expect(wrapper).toMatchSnapshot('closed');
+
+ click(wrapper.find('#status-trigger'));
+ expect(wrapper).toMatchSnapshot('open');
+
+ wrapper
+ .find(Toggler)
+ .props()
+ .onRequestClose();
+ expect(wrapper.find(DropdownOverlay).length).toBe(0);
+
+ expect(shallowRender({ hotspot: mockHotspot({ canChangeStatus: false }) })).toMatchSnapshot(
+ 'readonly'
+ );
+});
+
+it('should properly deal with status changes', () => {
+ const onStatusChange = jest.fn();
+ const wrapper = shallowRender({ onStatusChange });
+
+ click(wrapper.find('#status-trigger'));
+ wrapper
+ .find(Toggler)
+ .dive()
+ .find(StatusSelection)
+ .props()
+ .onStatusOptionChange(HotspotStatusOption.SAFE);
+ expect(onStatusChange).toHaveBeenCalled();
+ expect(wrapper.find(DropdownOverlay).length).toBe(0);
+});
+
+function shallowRender(props?: Partial<StatusProps>) {
+ return shallow<StatusProps>(
+ <Status
+ currentUser={mockCurrentUser({ isLoggedIn: true })}
+ hotspot={mockHotspot()}
+ onStatusChange={jest.fn()}
+ {...props}
+ />
+ );
+}
diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/status/__tests__/StatusDescription-test.tsx b/server/sonar-web/src/main/js/apps/security-hotspots/components/status/__tests__/StatusDescription-test.tsx
new file mode 100644
index 00000000000..048f5c6aec5
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/status/__tests__/StatusDescription-test.tsx
@@ -0,0 +1,35 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2020 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 { HotspotStatusOption } from '../../../../../types/security-hotspots';
+import StatusDescription, { StatusDescriptionProps } from '../StatusDescription';
+
+it('should render correctly', () => {
+ expect(shallowRender()).toMatchSnapshot();
+ expect(shallowRender({ showTitle: true })).toMatchSnapshot('with title');
+});
+
+function shallowRender(props?: Partial<StatusDescriptionProps>) {
+ return shallow<StatusDescriptionProps>(
+ <StatusDescription statusOption={HotspotStatusOption.TO_REVIEW} {...props} />
+ );
+}
diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/status/__tests__/StatusSelection-test.tsx b/server/sonar-web/src/main/js/apps/security-hotspots/components/status/__tests__/StatusSelection-test.tsx
new file mode 100644
index 00000000000..9b1d44c0fa8
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/status/__tests__/StatusSelection-test.tsx
@@ -0,0 +1,78 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2020 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 { waitAndUpdate } from 'sonar-ui-common/helpers/testUtils';
+import { setSecurityHotspotStatus } from '../../../../../api/security-hotspots';
+import { mockHotspot } from '../../../../../helpers/mocks/security-hotspots';
+import { HotspotStatus, HotspotStatusOption } from '../../../../../types/security-hotspots';
+import StatusSelection from '../StatusSelection';
+import StatusSelectionRenderer from '../StatusSelectionRenderer';
+
+jest.mock('../../../../../api/security-hotspots', () => ({
+ setSecurityHotspotStatus: jest.fn()
+}));
+
+it('should render correctly', () => {
+ expect(shallowRender()).toMatchSnapshot();
+});
+
+it('should properly deal with comment/status/submit events', async () => {
+ const hotspot = mockHotspot();
+ const onStatusOptionChange = jest.fn();
+ const wrapper = shallowRender({ hotspot, onStatusOptionChange });
+
+ const newStatusOption = HotspotStatusOption.SAFE;
+ wrapper
+ .find(StatusSelectionRenderer)
+ .props()
+ .onStatusChange(newStatusOption);
+ expect(wrapper.state().selectedStatus).toBe(newStatusOption);
+ expect(wrapper.find(StatusSelectionRenderer).props().submitDisabled).toBe(false);
+
+ const newComment = 'TEST-COMMENT';
+ wrapper
+ .find(StatusSelectionRenderer)
+ .props()
+ .onCommentChange(newComment);
+ expect(wrapper.state().comment).toBe(newComment);
+
+ (setSecurityHotspotStatus as jest.Mock).mockResolvedValueOnce({});
+ wrapper
+ .find(StatusSelectionRenderer)
+ .props()
+ .onSubmit();
+ expect(setSecurityHotspotStatus).toHaveBeenCalledWith(hotspot.key, {
+ status: HotspotStatus.REVIEWED,
+ resolution: HotspotStatusOption.SAFE,
+ comment: newComment
+ });
+
+ await waitAndUpdate(wrapper);
+
+ expect(onStatusOptionChange).toHaveBeenCalledWith(newStatusOption);
+});
+
+function shallowRender(props?: Partial<StatusSelection['props']>) {
+ return shallow<StatusSelection>(
+ <StatusSelection hotspot={mockHotspot()} onStatusOptionChange={jest.fn()} {...props} />
+ );
+}
diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/status/__tests__/StatusSelectionRenderer-test.tsx b/server/sonar-web/src/main/js/apps/security-hotspots/components/status/__tests__/StatusSelectionRenderer-test.tsx
new file mode 100644
index 00000000000..f63e47f45c6
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/status/__tests__/StatusSelectionRenderer-test.tsx
@@ -0,0 +1,72 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2020 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 { SubmitButton } from 'sonar-ui-common/components/controls/buttons';
+import Radio from 'sonar-ui-common/components/controls/Radio';
+import { change, click } from 'sonar-ui-common/helpers/testUtils';
+import { HotspotStatusOption } from '../../../../../types/security-hotspots';
+import StatusSelectionRenderer, { StatusSelectionRendererProps } from '../StatusSelectionRenderer';
+
+it('should render correctly', () => {
+ expect(shallowRender()).toMatchSnapshot();
+ expect(shallowRender({ loading: true })).toMatchSnapshot('loading');
+ expect(
+ shallowRender({ submitDisabled: true })
+ .find(SubmitButton)
+ .props().disabled
+ ).toBe(true);
+});
+
+it('should call proper callbacks on actions', () => {
+ const onCommentChange = jest.fn();
+ const onStatusChange = jest.fn();
+ const onSubmit = jest.fn();
+ const wrapper = shallowRender({ onCommentChange, onStatusChange, onSubmit });
+
+ change(wrapper.find('textarea'), 'TATA');
+ expect(onCommentChange).toHaveBeenCalledWith('TATA');
+
+ wrapper
+ .find(Radio)
+ .first()
+ .props()
+ .onCheck(HotspotStatusOption.SAFE);
+ expect(onStatusChange).toHaveBeenCalledWith(HotspotStatusOption.SAFE);
+
+ click(wrapper.find(SubmitButton));
+ expect(onSubmit).toHaveBeenCalled();
+});
+
+function shallowRender(props?: Partial<StatusSelectionRendererProps>) {
+ return shallow<StatusSelectionRendererProps>(
+ <StatusSelectionRenderer
+ comment="TEST-COMMENT"
+ loading={false}
+ onCommentChange={jest.fn()}
+ onStatusChange={jest.fn()}
+ onSubmit={jest.fn()}
+ selectedStatus={HotspotStatusOption.TO_REVIEW}
+ submitDisabled={false}
+ {...props}
+ />
+ );
+}
diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/status/__tests__/__snapshots__/Status-test.tsx.snap b/server/sonar-web/src/main/js/apps/security-hotspots/components/status/__tests__/__snapshots__/Status-test.tsx.snap
new file mode 100644
index 00000000000..36c5ed7da90
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/status/__tests__/__snapshots__/Status-test.tsx.snap
@@ -0,0 +1,436 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly: closed 1`] = `
+<div
+ className="dropdown huge-spacer-left"
+>
+ <Toggler
+ closeOnClickOutside={true}
+ closeOnEscape={true}
+ onRequestClose={[Function]}
+ open={false}
+ overlay={
+ <DropdownOverlay
+ noPadding={true}
+ placement="bottom"
+ >
+ <StatusSelection
+ 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 {
+ "breadcrumbs": Array [],
+ "key": "my-project",
+ "name": "MyProject",
+ "organization": "foo",
+ "qualifier": "FIL",
+ "qualityGate": Object {
+ "isDefault": true,
+ "key": "30",
+ "name": "Sonar way",
+ },
+ "qualityProfiles": Array [
+ Object {
+ "deleted": false,
+ "key": "my-qp",
+ "language": "ts",
+ "name": "Sonar way",
+ },
+ ],
+ "tags": Array [],
+ },
+ "creationDate": "2013-05-13T17:55:41+0200",
+ "key": "01fc972e-2a3c-433e-bcae-0bd7f88f5123",
+ "line": 142,
+ "message": "'3' is a magic number.",
+ "project": Object {
+ "breadcrumbs": Array [],
+ "key": "my-project",
+ "name": "MyProject",
+ "organization": "foo",
+ "qualifier": "TRK",
+ "qualityGate": Object {
+ "isDefault": true,
+ "key": "30",
+ "name": "Sonar way",
+ },
+ "qualityProfiles": Array [
+ Object {
+ "deleted": false,
+ "key": "my-qp",
+ "language": "ts",
+ "name": "Sonar way",
+ },
+ ],
+ "tags": Array [],
+ },
+ "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",
+ },
+ ],
+ }
+ }
+ onStatusOptionChange={[Function]}
+ />
+ </DropdownOverlay>
+ }
+ >
+ <div
+ aria-expanded={false}
+ aria-haspopup={true}
+ className="padded bordered display-flex-column display-flex-justify-center"
+ id="status-trigger"
+ onClick={[Function]}
+ role="button"
+ tabIndex={0}
+ >
+ <div
+ className="display-flex-center display-flex-space-between"
+ >
+ <StatusDescription
+ showTitle={true}
+ statusOption="FIXED"
+ />
+ <ChevronDownIcon
+ className="big-spacer-left"
+ />
+ </div>
+ </div>
+ </Toggler>
+</div>
+`;
+
+exports[`should render correctly: open 1`] = `
+<div
+ className="dropdown huge-spacer-left"
+>
+ <Toggler
+ closeOnClickOutside={true}
+ closeOnEscape={true}
+ onRequestClose={[Function]}
+ open={true}
+ overlay={
+ <DropdownOverlay
+ noPadding={true}
+ placement="bottom"
+ >
+ <StatusSelection
+ 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 {
+ "breadcrumbs": Array [],
+ "key": "my-project",
+ "name": "MyProject",
+ "organization": "foo",
+ "qualifier": "FIL",
+ "qualityGate": Object {
+ "isDefault": true,
+ "key": "30",
+ "name": "Sonar way",
+ },
+ "qualityProfiles": Array [
+ Object {
+ "deleted": false,
+ "key": "my-qp",
+ "language": "ts",
+ "name": "Sonar way",
+ },
+ ],
+ "tags": Array [],
+ },
+ "creationDate": "2013-05-13T17:55:41+0200",
+ "key": "01fc972e-2a3c-433e-bcae-0bd7f88f5123",
+ "line": 142,
+ "message": "'3' is a magic number.",
+ "project": Object {
+ "breadcrumbs": Array [],
+ "key": "my-project",
+ "name": "MyProject",
+ "organization": "foo",
+ "qualifier": "TRK",
+ "qualityGate": Object {
+ "isDefault": true,
+ "key": "30",
+ "name": "Sonar way",
+ },
+ "qualityProfiles": Array [
+ Object {
+ "deleted": false,
+ "key": "my-qp",
+ "language": "ts",
+ "name": "Sonar way",
+ },
+ ],
+ "tags": Array [],
+ },
+ "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",
+ },
+ ],
+ }
+ }
+ onStatusOptionChange={[Function]}
+ />
+ </DropdownOverlay>
+ }
+ >
+ <div
+ aria-expanded={true}
+ aria-haspopup={true}
+ className="padded bordered display-flex-column display-flex-justify-center"
+ id="status-trigger"
+ onClick={[Function]}
+ role="button"
+ tabIndex={0}
+ >
+ <div
+ className="display-flex-center display-flex-space-between"
+ >
+ <span
+ className="h3"
+ >
+ hotspots.status.select_status
+ </span>
+ <ChevronDownIcon
+ className="big-spacer-left"
+ />
+ </div>
+ </div>
+ </Toggler>
+</div>
+`;
+
+exports[`should render correctly: readonly 1`] = `
+<div
+ className="dropdown huge-spacer-left"
+>
+ <Tooltip
+ overlay="hotspots.status.cannot_change_status"
+ placement="bottom"
+ >
+ <Toggler
+ closeOnClickOutside={true}
+ closeOnEscape={true}
+ onRequestClose={[Function]}
+ open={false}
+ overlay={
+ <DropdownOverlay
+ noPadding={true}
+ placement="bottom"
+ >
+ <StatusSelection
+ 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": false,
+ "changelog": Array [],
+ "comment": Array [],
+ "component": Object {
+ "breadcrumbs": Array [],
+ "key": "my-project",
+ "name": "MyProject",
+ "organization": "foo",
+ "qualifier": "FIL",
+ "qualityGate": Object {
+ "isDefault": true,
+ "key": "30",
+ "name": "Sonar way",
+ },
+ "qualityProfiles": Array [
+ Object {
+ "deleted": false,
+ "key": "my-qp",
+ "language": "ts",
+ "name": "Sonar way",
+ },
+ ],
+ "tags": Array [],
+ },
+ "creationDate": "2013-05-13T17:55:41+0200",
+ "key": "01fc972e-2a3c-433e-bcae-0bd7f88f5123",
+ "line": 142,
+ "message": "'3' is a magic number.",
+ "project": Object {
+ "breadcrumbs": Array [],
+ "key": "my-project",
+ "name": "MyProject",
+ "organization": "foo",
+ "qualifier": "TRK",
+ "qualityGate": Object {
+ "isDefault": true,
+ "key": "30",
+ "name": "Sonar way",
+ },
+ "qualityProfiles": Array [
+ Object {
+ "deleted": false,
+ "key": "my-qp",
+ "language": "ts",
+ "name": "Sonar way",
+ },
+ ],
+ "tags": Array [],
+ },
+ "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",
+ },
+ ],
+ }
+ }
+ onStatusOptionChange={[Function]}
+ />
+ </DropdownOverlay>
+ }
+ >
+ <div
+ aria-expanded={false}
+ aria-haspopup={true}
+ className="padded bordered display-flex-column display-flex-justify-center readonly"
+ id="status-trigger"
+ onClick={[Function]}
+ role="button"
+ tabIndex={0}
+ >
+ <div
+ className="display-flex-center display-flex-space-between"
+ >
+ <StatusDescription
+ showTitle={true}
+ statusOption="FIXED"
+ />
+ </div>
+ </div>
+ </Toggler>
+ </Tooltip>
+</div>
+`;
diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/status/__tests__/__snapshots__/StatusDescription-test.tsx.snap b/server/sonar-web/src/main/js/apps/security-hotspots/components/status/__tests__/__snapshots__/StatusDescription-test.tsx.snap
new file mode 100644
index 00000000000..b77051910de
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/status/__tests__/__snapshots__/StatusDescription-test.tsx.snap
@@ -0,0 +1,24 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly 1`] = `
+<div>
+ <h3>
+ hotspots.status_option.TO_REVIEW
+ </h3>
+ <span>
+ hotspots.status_option.TO_REVIEW.description
+ </span>
+</div>
+`;
+
+exports[`should render correctly: with title 1`] = `
+<div>
+ <h3>
+ status:
+ hotspots.status_option.TO_REVIEW
+ </h3>
+ <span>
+ hotspots.status_option.TO_REVIEW.description
+ </span>
+</div>
+`;
diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/status/__tests__/__snapshots__/StatusSelection-test.tsx.snap b/server/sonar-web/src/main/js/apps/security-hotspots/components/status/__tests__/__snapshots__/StatusSelection-test.tsx.snap
new file mode 100644
index 00000000000..61d22f512e6
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/status/__tests__/__snapshots__/StatusSelection-test.tsx.snap
@@ -0,0 +1,12 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly 1`] = `
+<StatusSelectionRenderer
+ loading={false}
+ onCommentChange={[Function]}
+ onStatusChange={[Function]}
+ onSubmit={[Function]}
+ selectedStatus="FIXED"
+ submitDisabled={true}
+/>
+`;
diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/status/__tests__/__snapshots__/StatusSelectionRenderer-test.tsx.snap b/server/sonar-web/src/main/js/apps/security-hotspots/components/status/__tests__/__snapshots__/StatusSelectionRenderer-test.tsx.snap
new file mode 100644
index 00000000000..482dd86bb54
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/status/__tests__/__snapshots__/StatusSelectionRenderer-test.tsx.snap
@@ -0,0 +1,140 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly 1`] = `
+<Fragment>
+ <div
+ className="big-padded"
+ >
+ <Radio
+ checked={true}
+ className="big-spacer-bottom"
+ onCheck={[MockFunction]}
+ value="TO_REVIEW"
+ >
+ <StatusDescription
+ statusOption="TO_REVIEW"
+ />
+ </Radio>
+ <Radio
+ checked={false}
+ className="big-spacer-bottom"
+ onCheck={[MockFunction]}
+ value="FIXED"
+ >
+ <StatusDescription
+ statusOption="FIXED"
+ />
+ </Radio>
+ <Radio
+ checked={false}
+ className="big-spacer-bottom"
+ onCheck={[MockFunction]}
+ value="SAFE"
+ >
+ <StatusDescription
+ statusOption="SAFE"
+ />
+ </Radio>
+ </div>
+ <hr />
+ <div
+ className="big-padded display-flex-column"
+ >
+ <label
+ className="text-bold"
+ htmlFor="comment-textarea"
+ >
+ hotspots.status.add_comment
+ </label>
+ <textarea
+ className="spacer-top form-field fixed-width spacer-bottom"
+ id="comment-textarea"
+ onChange={[Function]}
+ rows={4}
+ value="TEST-COMMENT"
+ />
+ <MarkdownTips />
+ <div
+ className="big-spacer-top display-flex-justify-end display-flex-center"
+ >
+ <SubmitButton
+ disabled={false}
+ onClick={[MockFunction]}
+ >
+ hotspots.status.change_status
+ </SubmitButton>
+ </div>
+ </div>
+</Fragment>
+`;
+
+exports[`should render correctly: loading 1`] = `
+<Fragment>
+ <div
+ className="big-padded"
+ >
+ <Radio
+ checked={true}
+ className="big-spacer-bottom"
+ onCheck={[MockFunction]}
+ value="TO_REVIEW"
+ >
+ <StatusDescription
+ statusOption="TO_REVIEW"
+ />
+ </Radio>
+ <Radio
+ checked={false}
+ className="big-spacer-bottom"
+ onCheck={[MockFunction]}
+ value="FIXED"
+ >
+ <StatusDescription
+ statusOption="FIXED"
+ />
+ </Radio>
+ <Radio
+ checked={false}
+ className="big-spacer-bottom"
+ onCheck={[MockFunction]}
+ value="SAFE"
+ >
+ <StatusDescription
+ statusOption="SAFE"
+ />
+ </Radio>
+ </div>
+ <hr />
+ <div
+ className="big-padded display-flex-column"
+ >
+ <label
+ className="text-bold"
+ htmlFor="comment-textarea"
+ >
+ hotspots.status.add_comment
+ </label>
+ <textarea
+ className="spacer-top form-field fixed-width spacer-bottom"
+ id="comment-textarea"
+ onChange={[Function]}
+ rows={4}
+ value="TEST-COMMENT"
+ />
+ <MarkdownTips />
+ <div
+ className="big-spacer-top display-flex-justify-end display-flex-center"
+ >
+ <SubmitButton
+ disabled={true}
+ onClick={[MockFunction]}
+ >
+ hotspots.status.change_status
+ </SubmitButton>
+ <i
+ className="spacer-left spinner"
+ />
+ </div>
+ </div>
+</Fragment>
+`;