From 037ef05f53f198ec33bd7d5ecf4fa057d87a2a4d Mon Sep 17 00:00:00 2001 From: Jeremy Date: Thu, 19 Dec 2019 19:18:21 +0100 Subject: [PATCH] SONAR-12754 Enable hotspot assignment --- .../src/main/js/api/security-hotspots.ts | 17 +- .../src/main/js/app/styles/init/misc.css | 4 + .../components/HotspotActionsForm.tsx | 30 +++- .../components/HotspotActionsFormRenderer.tsx | 9 + .../components/HotspotAssigneeSelect.css | 27 +++ .../components/HotspotAssigneeSelect.tsx | 158 ++++++++++++++++++ .../HotspotAssigneeSelectRenderer.tsx | 88 ++++++++++ .../__tests__/HotspotActionsForm-test.tsx | 34 +++- .../HotspotActionsFormRenderer-test.tsx | 8 + .../__tests__/HotspotAssigneeSelect-test.tsx | 97 +++++++++++ .../HotspotAssigneeSelectRenderer-test.tsx | 70 ++++++++ .../HotspotActionsForm-test.tsx.snap | 1 + .../HotspotActionsFormRenderer-test.tsx.snap | 88 ++++++++++ .../HotspotAssigneeSelect-test.tsx.snap | 11 ++ ...otspotAssigneeSelectRenderer-test.tsx.snap | 101 +++++++++++ .../src/main/js/types/security-hotspots.ts | 6 +- 16 files changed, 735 insertions(+), 14 deletions(-) create mode 100644 server/sonar-web/src/main/js/apps/securityHotspots/components/HotspotAssigneeSelect.css create mode 100644 server/sonar-web/src/main/js/apps/securityHotspots/components/HotspotAssigneeSelect.tsx create mode 100644 server/sonar-web/src/main/js/apps/securityHotspots/components/HotspotAssigneeSelectRenderer.tsx create mode 100644 server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/HotspotAssigneeSelect-test.tsx create mode 100644 server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/HotspotAssigneeSelectRenderer-test.tsx create mode 100644 server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/__snapshots__/HotspotAssigneeSelect-test.tsx.snap create mode 100644 server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/__snapshots__/HotspotAssigneeSelectRenderer-test.tsx.snap diff --git a/server/sonar-web/src/main/js/api/security-hotspots.ts b/server/sonar-web/src/main/js/api/security-hotspots.ts index 316166e0f8d..cea427e8c12 100644 --- a/server/sonar-web/src/main/js/api/security-hotspots.ts +++ b/server/sonar-web/src/main/js/api/security-hotspots.ts @@ -22,12 +22,25 @@ import throwGlobalError from '../app/utils/throwGlobalError'; import { BranchParameters } from '../types/branch-like'; import { DetailedHotspot, + HotspotAssignRequest, HotspotSearchResponse, HotspotSetStatusRequest } from '../types/security-hotspots'; -export function setSecurityHotspotStatus(data: HotspotSetStatusRequest): Promise { - return post('/api/hotspots/change_status', data).catch(throwGlobalError); +export function assignSecurityHotspot( + hotspotKey: string, + data: HotspotAssignRequest +): Promise { + return post('/api/hotspots/assign', { hotspot: hotspotKey, ...data }).catch(throwGlobalError); +} + +export function setSecurityHotspotStatus( + hotspotKey: string, + data: HotspotSetStatusRequest +): Promise { + return post('/api/hotspots/change_status', { hotspot: hotspotKey, ...data }).catch( + throwGlobalError + ); } export function getSecurityHotspots( diff --git a/server/sonar-web/src/main/js/app/styles/init/misc.css b/server/sonar-web/src/main/js/app/styles/init/misc.css index 9c8de00be8e..620e05f33e7 100644 --- a/server/sonar-web/src/main/js/app/styles/init/misc.css +++ b/server/sonar-web/src/main/js/app/styles/init/misc.css @@ -132,6 +132,10 @@ th.hide-overflow { margin-top: 4px !important; } +.padded { + padding: var(--gridSize); +} + .big-padded { padding: calc(2 * var(--gridSize)); } diff --git a/server/sonar-web/src/main/js/apps/securityHotspots/components/HotspotActionsForm.tsx b/server/sonar-web/src/main/js/apps/securityHotspots/components/HotspotActionsForm.tsx index 97713831d4c..b1c998847b5 100644 --- a/server/sonar-web/src/main/js/apps/securityHotspots/components/HotspotActionsForm.tsx +++ b/server/sonar-web/src/main/js/apps/securityHotspots/components/HotspotActionsForm.tsx @@ -18,7 +18,7 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; -import { setSecurityHotspotStatus } from '../../../api/security-hotspots'; +import { assignSecurityHotspot, setSecurityHotspotStatus } from '../../../api/security-hotspots'; import { HotspotResolution, HotspotSetStatusRequest, @@ -34,6 +34,7 @@ interface Props { } interface State { + selectedUser?: T.UserActive; selectedOption: HotspotStatusOptions; submitting: boolean; } @@ -48,6 +49,10 @@ export default class HotspotActionsForm extends React.Component { this.setState({ selectedOption }); }; + handleAssign = (selectedUser: T.UserActive) => { + this.setState({ selectedUser }); + }; + handleSubmit = (event: React.SyntheticEvent) => { event.preventDefault(); @@ -58,13 +63,20 @@ export default class HotspotActionsForm extends React.Component { selectedOption === HotspotStatusOptions.ADDITIONAL_REVIEW ? HotspotStatus.TO_REVIEW : HotspotStatus.REVIEWED; - const data: HotspotSetStatusRequest = { hotspot: hotspotKey, status }; + const data: HotspotSetStatusRequest = { status }; if (selectedOption !== HotspotStatusOptions.ADDITIONAL_REVIEW) { data.resolution = HotspotResolution[selectedOption]; } this.setState({ submitting: true }); - return setSecurityHotspotStatus(data) + return setSecurityHotspotStatus(hotspotKey, data) + .then(() => { + const { selectedUser } = this.state; + if (selectedOption === HotspotStatusOptions.ADDITIONAL_REVIEW && selectedUser) { + return this.assignHotspot(selectedUser); + } + return null; + }) .then(() => { this.props.onSubmit({ status, resolution: data.resolution }); }) @@ -73,16 +85,26 @@ export default class HotspotActionsForm extends React.Component { }); }; + assignHotspot = (assignee: T.UserActive) => { + const { hotspotKey } = this.props; + + return assignSecurityHotspot(hotspotKey, { + assignee: assignee.login + }); + }; + render() { const { hotspotKey } = this.props; - const { selectedOption, submitting } = this.state; + const { selectedOption, selectedUser, submitting } = this.state; return ( ); diff --git a/server/sonar-web/src/main/js/apps/securityHotspots/components/HotspotActionsFormRenderer.tsx b/server/sonar-web/src/main/js/apps/securityHotspots/components/HotspotActionsFormRenderer.tsx index 666c8a809c2..8fb0824385d 100644 --- a/server/sonar-web/src/main/js/apps/securityHotspots/components/HotspotActionsFormRenderer.tsx +++ b/server/sonar-web/src/main/js/apps/securityHotspots/components/HotspotActionsFormRenderer.tsx @@ -22,12 +22,15 @@ 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 { HotspotStatusOptions } from '../../../types/security-hotspots'; +import HotspotAssigneeSelect from './HotspotAssigneeSelect'; export interface HotspotActionsFormRendererProps { hotspotKey: string; + onAssign: (user: T.UserActive) => void; onSelectOption: (option: HotspotStatusOptions) => void; onSubmit: (event: React.SyntheticEvent) => void; selectedOption: HotspotStatusOptions; + selectedUser?: T.UserActive; submitting: boolean; } @@ -54,6 +57,12 @@ export default function HotspotActionsFormRenderer(props: HotspotActionsFormRend onClick: props.onSelectOption })} + {selectedOption === HotspotStatusOptions.ADDITIONAL_REVIEW && ( +
+ + +
+ )}
{submitting && } {translate('hotspots.form.submit')} diff --git a/server/sonar-web/src/main/js/apps/securityHotspots/components/HotspotAssigneeSelect.css b/server/sonar-web/src/main/js/apps/securityHotspots/components/HotspotAssigneeSelect.css new file mode 100644 index 00000000000..7a5c24ebf0e --- /dev/null +++ b/server/sonar-web/src/main/js/apps/securityHotspots/components/HotspotAssigneeSelect.css @@ -0,0 +1,27 @@ +/* + * 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. + */ +.hotspot-assignee-search-results li { + cursor: pointer; +} + +.hotspot-assignee-search-results li:hover, +.hotspot-assignee-search-results li.active { + background-color: var(--barBackgroundColor); +} diff --git a/server/sonar-web/src/main/js/apps/securityHotspots/components/HotspotAssigneeSelect.tsx b/server/sonar-web/src/main/js/apps/securityHotspots/components/HotspotAssigneeSelect.tsx new file mode 100644 index 00000000000..e2d16a1edf8 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/securityHotspots/components/HotspotAssigneeSelect.tsx @@ -0,0 +1,158 @@ +/* + * 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 { debounce } from 'lodash'; +import * as React from 'react'; +import { KeyCodes } from 'sonar-ui-common/helpers/keycodes'; +import { searchUsers } from '../../../api/users'; +import { isUserActive } from '../../../helpers/users'; +import HotspotAssigneeSelectRenderer from './HotspotAssigneeSelectRenderer'; + +interface Props { + onSelect: (user: T.UserActive) => void; +} + +interface State { + highlighted?: T.UserActive; + loading: boolean; + open: boolean; + query?: string; + suggestedUsers?: T.UserActive[]; +} + +export default class HotspotAssigneeSelect extends React.PureComponent { + state: State; + + constructor(props: Props) { + super(props); + this.state = { + loading: false, + open: false + }; + this.handleSearch = debounce(this.handleSearch, 250); + } + + getCurrentIndex = () => { + const { highlighted, suggestedUsers } = this.state; + return highlighted && suggestedUsers + ? suggestedUsers.findIndex(suggestion => suggestion.login === highlighted.login) + : -1; + }; + + handleSearch = (query: string) => { + if (query.length < 2) { + this.setState({ open: false, query }); + return Promise.resolve([]); + } + + this.setState({ loading: true, query }); + return searchUsers({ q: query }) + .then(this.handleSearchResult, () => {}) + .catch(() => this.setState({ loading: false })); + }; + + handleSearchResult = ({ users }: { users: T.UserBase[] }) => { + const activeUsers = users.filter(isUserActive); + this.setState(({ highlighted }) => { + if (activeUsers.length === 0) { + highlighted = undefined; + } else { + const findHighlited = activeUsers.find(u => highlighted && u.login === highlighted.login); + highlighted = findHighlited || activeUsers[0]; + } + + return { + highlighted, + loading: false, + open: true, + suggestedUsers: activeUsers + }; + }); + }; + + handleKeyDown = (event: React.KeyboardEvent) => { + switch (event.keyCode) { + case KeyCodes.Enter: + event.preventDefault(); + this.handleSelectHighlighted(); + break; + case KeyCodes.UpArrow: + event.preventDefault(); + this.handleHighlightPrevious(); + break; + case KeyCodes.DownArrow: + event.preventDefault(); + this.handleHighlightNext(); + break; + } + }; + + highlightIndex = (index: number) => { + const { suggestedUsers } = this.state; + if (suggestedUsers && suggestedUsers.length > 0) { + if (index < 0) { + index = suggestedUsers.length - 1; + } else if (index >= suggestedUsers.length) { + index = 0; + } + this.setState({ + highlighted: suggestedUsers[index] + }); + } + }; + + handleHighlightPrevious = () => { + this.highlightIndex(this.getCurrentIndex() - 1); + }; + + handleHighlightNext = () => { + this.highlightIndex(this.getCurrentIndex() + 1); + }; + + handleSelectHighlighted = () => { + const { highlighted } = this.state; + if (highlighted !== undefined) { + this.handleSelect(highlighted); + } + }; + + handleSelect = (selectedUser: T.UserActive) => { + this.setState({ + open: false, + query: selectedUser.name + }); + this.props.onSelect(selectedUser); + }; + + render() { + const { highlighted, loading, open, query, suggestedUsers } = this.state; + return ( + + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/securityHotspots/components/HotspotAssigneeSelectRenderer.tsx b/server/sonar-web/src/main/js/apps/securityHotspots/components/HotspotAssigneeSelectRenderer.tsx new file mode 100644 index 00000000000..2df77348d12 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/securityHotspots/components/HotspotAssigneeSelectRenderer.tsx @@ -0,0 +1,88 @@ +/* + * 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 SearchBox from 'sonar-ui-common/components/controls/SearchBox'; +import DeferredSpinner from 'sonar-ui-common/components/ui/DeferredSpinner'; +import { PopupPlacement } from 'sonar-ui-common/components/ui/popups'; +import { translate } from 'sonar-ui-common/helpers/l10n'; +import Avatar from '../../../components/ui/Avatar'; +import './HotspotAssigneeSelect.css'; + +export interface HotspotAssigneeSelectRendererProps { + highlighted?: T.UserActive; + loading: boolean; + onKeyDown: (event: React.KeyboardEvent) => void; + onSearch: (query: string) => void; + onSelect: (user: T.UserActive) => void; + open: boolean; + query?: string; + suggestedUsers?: T.UserActive[]; +} + +export default function HotspotAssigneeSelectRenderer(props: HotspotAssigneeSelectRendererProps) { + const { highlighted, loading, open, query, suggestedUsers } = props; + return ( + <> + + + {loading && } + + {!loading && open && ( +
+ + {suggestedUsers && suggestedUsers.length > 0 ? ( +
    + {suggestedUsers.map(suggestion => ( +
  • props.onSelect(suggestion)}> + + {suggestion.name} +
  • + ))} +
+ ) : ( +
{translate('no_results')}
+ )} +
+
+ )} + + ); +} diff --git a/server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/HotspotActionsForm-test.tsx b/server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/HotspotActionsForm-test.tsx index b4add87f98c..a371d5efa12 100644 --- a/server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/HotspotActionsForm-test.tsx +++ b/server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/HotspotActionsForm-test.tsx @@ -20,7 +20,8 @@ import { shallow } from 'enzyme'; import * as React from 'react'; import { waitAndUpdate } from 'sonar-ui-common/helpers/testUtils'; -import { setSecurityHotspotStatus } from '../../../../api/security-hotspots'; +import { assignSecurityHotspot, setSecurityHotspotStatus } from '../../../../api/security-hotspots'; +import { mockLoggedInUser } from '../../../../helpers/testMocks'; import { HotspotResolution, HotspotStatus, @@ -29,6 +30,7 @@ import { import HotspotActionsForm from '../HotspotActionsForm'; jest.mock('../../../../api/security-hotspots', () => ({ + assignSecurityHotspot: jest.fn().mockResolvedValue(undefined), setSecurityHotspotStatus: jest.fn().mockResolvedValue(undefined) })); @@ -55,8 +57,7 @@ it('should handle submit', async () => { expect(wrapper.state().submitting).toBe(true); await promise; - expect(setSecurityHotspotStatus).toBeCalledWith({ - hotspot: 'key', + expect(setSecurityHotspotStatus).toBeCalledWith('key', { status: HotspotStatus.TO_REVIEW }); expect(onSubmit).toBeCalled(); @@ -65,8 +66,7 @@ it('should handle submit', async () => { wrapper.setState({ selectedOption: HotspotStatusOptions.SAFE }); await waitAndUpdate(wrapper); await wrapper.instance().handleSubmit({ preventDefault } as any); - expect(setSecurityHotspotStatus).toBeCalledWith({ - hotspot: 'key', + expect(setSecurityHotspotStatus).toBeCalledWith('key', { status: HotspotStatus.REVIEWED, resolution: HotspotResolution.SAFE }); @@ -75,13 +75,33 @@ it('should handle submit', async () => { wrapper.setState({ selectedOption: HotspotStatusOptions.FIXED }); await waitAndUpdate(wrapper); await wrapper.instance().handleSubmit({ preventDefault } as any); - expect(setSecurityHotspotStatus).toBeCalledWith({ - hotspot: 'key', + expect(setSecurityHotspotStatus).toBeCalledWith('key', { status: HotspotStatus.REVIEWED, resolution: HotspotResolution.FIXED }); }); +it('should handle assignment', async () => { + const onSubmit = jest.fn(); + const wrapper = shallowRender({ onSubmit }); + wrapper.setState({ selectedOption: HotspotStatusOptions.ADDITIONAL_REVIEW }); + wrapper.instance().handleAssign(mockLoggedInUser({ login: 'userLogin' })); + await waitAndUpdate(wrapper); + + const promise = wrapper.instance().handleSubmit({ preventDefault: jest.fn() } as any); + + expect(wrapper.state().submitting).toBe(true); + await promise; + + expect(setSecurityHotspotStatus).toBeCalledWith('key', { + status: HotspotStatus.TO_REVIEW + }); + expect(assignSecurityHotspot).toBeCalledWith('key', { + assignee: 'userLogin' + }); + expect(onSubmit).toBeCalled(); +}); + it('should handle submit failure', async () => { const onSubmit = jest.fn(); (setSecurityHotspotStatus as jest.Mock).mockRejectedValueOnce('failure'); diff --git a/server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/HotspotActionsFormRenderer-test.tsx b/server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/HotspotActionsFormRenderer-test.tsx index b7372b04035..ff65d450a0f 100644 --- a/server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/HotspotActionsFormRenderer-test.tsx +++ b/server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/HotspotActionsFormRenderer-test.tsx @@ -19,6 +19,7 @@ */ import { shallow } from 'enzyme'; import * as React from 'react'; +import { mockLoggedInUser } from '../../../../helpers/testMocks'; import { HotspotStatusOptions } from '../../../../types/security-hotspots'; import HotspotActionsForm from '../HotspotActionsForm'; import HotspotActionsFormRenderer, { @@ -31,12 +32,19 @@ it('should render correctly', () => { expect(shallowRender({ selectedOption: HotspotStatusOptions.SAFE })).toMatchSnapshot( 'safe option selected' ); + expect( + shallowRender({ + selectedOption: HotspotStatusOptions.ADDITIONAL_REVIEW, + selectedUser: mockLoggedInUser() + }) + ).toMatchSnapshot('user selected'); }); function shallowRender(props: Partial = {}) { return shallow( ({ + searchUsers: jest.fn().mockResolvedValue([]) +})); + +it('should render correctly', () => { + expect(shallowRender()).toMatchSnapshot(); +}); + +it('should handle keydown', () => { + const mockEvent = (keyCode: number) => ({ preventDefault: jest.fn(), keyCode }); + const suggestedUsers = [ + mockUser({ login: '1' }) as T.UserActive, + mockUser({ login: '2' }) as T.UserActive, + mockUser({ login: '3' }) as T.UserActive + ]; + + const onSelect = jest.fn(); + const wrapper = shallowRender({ onSelect }); + + wrapper.instance().handleKeyDown(mockEvent(KeyCodes.UpArrow) as any); + expect(wrapper.state().highlighted).toBeUndefined(); + + wrapper.setState({ suggestedUsers }); + + // press down to highlight the first + wrapper.instance().handleKeyDown(mockEvent(KeyCodes.DownArrow) as any); + expect(wrapper.state().highlighted).toBe(suggestedUsers[0]); + + // press up to loop around to last + wrapper.instance().handleKeyDown(mockEvent(KeyCodes.UpArrow) as any); + expect(wrapper.state().highlighted).toBe(suggestedUsers[2]); + + // press down to loop around to first + wrapper.instance().handleKeyDown(mockEvent(KeyCodes.DownArrow) as any); + expect(wrapper.state().highlighted).toBe(suggestedUsers[0]); + + // press down highlight the next + wrapper.instance().handleKeyDown(mockEvent(KeyCodes.DownArrow) as any); + expect(wrapper.state().highlighted).toBe(suggestedUsers[1]); + + // press enter to select the highlighted user + wrapper.instance().handleKeyDown(mockEvent(KeyCodes.Enter) as any); + expect(onSelect).toBeCalledWith(suggestedUsers[1]); +}); + +it('should handle search', async () => { + const users = [mockUser({ login: '1' }), mockUser({ login: '2' }), mockUser({ login: '3' })]; + (searchUsers as jest.Mock).mockResolvedValueOnce({ users }); + + const wrapper = shallowRender(); + wrapper.instance().handleSearch('j'); + + expect(searchUsers).not.toBeCalled(); + expect(wrapper.state().open).toBe(false); + + wrapper.instance().handleSearch('jo'); + expect(wrapper.state().loading).toBe(true); + expect(searchUsers).toBeCalledWith({ q: 'jo' }); + + await waitAndUpdate(wrapper); + + expect(wrapper.state().highlighted).toBe(users[0]); + expect(wrapper.state().loading).toBe(false); + expect(wrapper.state().open).toBe(true); + expect(wrapper.state().suggestedUsers).toHaveLength(3); +}); + +function shallowRender(props?: Partial) { + return shallow(); +} diff --git a/server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/HotspotAssigneeSelectRenderer-test.tsx b/server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/HotspotAssigneeSelectRenderer-test.tsx new file mode 100644 index 00000000000..c067c3dcfa0 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/HotspotAssigneeSelectRenderer-test.tsx @@ -0,0 +1,70 @@ +/* + * 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 { mockUser } from '../../../../helpers/testMocks'; +import HotspotAssigneeSelectRenderer, { + HotspotAssigneeSelectRendererProps +} from '../HotspotAssigneeSelectRenderer'; + +it('should render correctly', () => { + expect(shallowRender()).toMatchSnapshot(); + expect(shallowRender({ loading: true })).toMatchSnapshot('loading'); + expect(shallowRender({ open: true })).toMatchSnapshot('open'); + + const highlightedUser = mockUser({ login: 'highlighted' }) as T.UserActive; + expect( + shallowRender({ + highlighted: highlightedUser, + open: true, + suggestedUsers: [mockUser() as T.UserActive, highlightedUser] + }) + ).toMatchSnapshot('open with results'); +}); + +it('should call onSelect when clicked', () => { + const user = mockUser() as T.UserActive; + const onSelect = jest.fn(); + const wrapper = shallowRender({ + open: true, + onSelect, + suggestedUsers: [user] + }); + + wrapper + .find('li') + .at(0) + .simulate('click'); + + expect(onSelect).toBeCalledWith(user); +}); + +function shallowRender(props?: Partial) { + return shallow( + + ); +} diff --git a/server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/__snapshots__/HotspotActionsForm-test.tsx.snap b/server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/__snapshots__/HotspotActionsForm-test.tsx.snap index ef7582d5434..68ed8d32c27 100644 --- a/server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/__snapshots__/HotspotActionsForm-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/__snapshots__/HotspotActionsForm-test.tsx.snap @@ -3,6 +3,7 @@ exports[`should render correctly 1`] = ` `; + +exports[`should render correctly: user selected 1`] = ` +
+

+ hotspots.form.title +

+
+
+ +

+ hotspots.status_option.FIXED +

+
+
+ hotspots.status_option.FIXED.description +
+
+
+ +

+ hotspots.status_option.SAFE +

+
+
+ hotspots.status_option.SAFE.description +
+
+
+ +

+ hotspots.status_option.ADDITIONAL_REVIEW +

+
+
+ hotspots.status_option.ADDITIONAL_REVIEW.description +
+
+
+
+ + +
+
+ + hotspots.form.submit + +
+
+`; diff --git a/server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/__snapshots__/HotspotAssigneeSelect-test.tsx.snap b/server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/__snapshots__/HotspotAssigneeSelect-test.tsx.snap new file mode 100644 index 00000000000..3d3a988ea50 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/__snapshots__/HotspotAssigneeSelect-test.tsx.snap @@ -0,0 +1,11 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should render correctly 1`] = ` + +`; diff --git a/server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/__snapshots__/HotspotAssigneeSelectRenderer-test.tsx.snap b/server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/__snapshots__/HotspotAssigneeSelectRenderer-test.tsx.snap new file mode 100644 index 00000000000..f86f59f3bdc --- /dev/null +++ b/server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/__snapshots__/HotspotAssigneeSelectRenderer-test.tsx.snap @@ -0,0 +1,101 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should render correctly 1`] = ` + + + +`; + +exports[`should render correctly: loading 1`] = ` + + + + +`; + +exports[`should render correctly: open 1`] = ` + + +
+ +
+ no_results +
+
+
+
+`; + +exports[`should render correctly: open with results 1`] = ` + + +
+ +
    +
  • + + John Doe +
  • +
  • + + John Doe +
  • +
+
+
+
+`; diff --git a/server/sonar-web/src/main/js/types/security-hotspots.ts b/server/sonar-web/src/main/js/types/security-hotspots.ts index 13b8a12bb01..1a137246339 100644 --- a/server/sonar-web/src/main/js/types/security-hotspots.ts +++ b/server/sonar-web/src/main/js/types/security-hotspots.ts @@ -99,7 +99,11 @@ export interface HotspotSearchResponse { } export interface HotspotSetStatusRequest { - hotspot: string; status: HotspotStatus; resolution?: HotspotResolution; } + +export interface HotspotAssignRequest { + assignee: string; + comment?: string; +} -- 2.39.5