diff options
Diffstat (limited to 'server/sonar-web/src/main/js/apps/security-hotspots/components/status')
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> +`; |