diff options
author | philippe-perrin-sonarsource <philippe.perrin@sonarsource.com> | 2019-06-20 17:11:01 +0200 |
---|---|---|
committer | sonartech <sonartech@sonarsource.com> | 2019-06-28 08:45:50 +0200 |
commit | c8aad7e3ccdb94828808d7b2780be2ad8ead3436 (patch) | |
tree | 1f06e7ef510d192ba1bf8de59e1a3de71e677bce | |
parent | 72d3203e113f99ac3aca1be5d2998343d61dbb53 (diff) | |
download | sonarqube-c8aad7e3ccdb94828808d7b2780be2ad8ead3436.tar.gz sonarqube-c8aad7e3ccdb94828808d7b2780be2ad8ead3436.zip |
SONAR-12210 Handle pagination in user & group list
11 files changed, 549 insertions, 128 deletions
diff --git a/server/sonar-web/src/main/js/api/user_groups.ts b/server/sonar-web/src/main/js/api/user_groups.ts index a84be34b5ea..eae50459ee1 100644 --- a/server/sonar-web/src/main/js/api/user_groups.ts +++ b/server/sonar-web/src/main/js/api/user_groups.ts @@ -44,7 +44,7 @@ export function getUsersInGroup(data: { ps?: number; q?: string; selected?: string; -}): Promise<{ paging: T.Paging; users: GroupUser[] }> { +}): Promise<T.Paging & { users: GroupUser[] }> { return getJSON('/api/user_groups/users', data).catch(throwGlobalError); } diff --git a/server/sonar-web/src/main/js/api/users.ts b/server/sonar-web/src/main/js/api/users.ts index 884a057da08..8daacb90b25 100644 --- a/server/sonar-web/src/main/js/api/users.ts +++ b/server/sonar-web/src/main/js/api/users.ts @@ -17,7 +17,7 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import { getJSON, post, postJSON, RequestData } from '../helpers/request'; +import { getJSON, post, postJSON } from '../helpers/request'; import throwGlobalError from '../app/utils/throwGlobalError'; export function getCurrentUser(): Promise<T.CurrentUser> { @@ -40,22 +40,14 @@ export interface UserGroup { selected: boolean; } -export function getUserGroups( - login: string, - organization?: string, - query?: string, - selected?: string -): Promise<{ paging: T.Paging; groups: UserGroup[] }> { - const data: RequestData = { login }; - if (organization) { - data.organization = organization; - } - if (query) { - data.q = query; - } - if (selected) { - data.selected = selected; - } +export function getUserGroups(data: { + login: string; + organization?: string; + p?: number; + ps?: number; + q?: string; + selected?: string; +}): Promise<{ paging: T.Paging; groups: UserGroup[] }> { return getJSON('/api/users/groups', data); } diff --git a/server/sonar-web/src/main/js/apps/groups/components/EditMembersModal.tsx b/server/sonar-web/src/main/js/apps/groups/components/EditMembersModal.tsx index 14b4c813d14..9b475b87c6a 100644 --- a/server/sonar-web/src/main/js/apps/groups/components/EditMembersModal.tsx +++ b/server/sonar-web/src/main/js/apps/groups/components/EditMembersModal.tsx @@ -17,19 +17,19 @@ * 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 { find, without } from 'lodash'; -import Modal from '../../../components/controls/Modal'; -import SelectList, { Filter } from '../../../components/SelectList/SelectList'; -import { ResetButtonLink } from '../../../components/ui/buttons'; -import { translate } from '../../../helpers/l10n'; +import * as React from 'react'; import { - GroupUser, - removeUserFromGroup, addUserToGroup, - getUsersInGroup + getUsersInGroup, + GroupUser, + removeUserFromGroup } from '../../../api/user_groups'; import DeferredSpinner from '../../../components/common/DeferredSpinner'; +import Modal from '../../../components/controls/Modal'; +import SelectList, { Filter } from '../../../components/SelectList/SelectList'; +import { ResetButtonLink } from '../../../components/ui/buttons'; +import { translate } from '../../../helpers/l10n'; interface Props { group: T.Group; @@ -37,39 +37,83 @@ interface Props { organization: string | undefined; } +export interface SearchParams { + name: string; + organization?: string; + page: number; + pageSize: number; + query?: string; + selected: string; +} + interface State { + lastSearchParams: SearchParams; + listHasBeenTouched: boolean; loading: boolean; users: GroupUser[]; + usersTotalCount?: number; selectedUsers: string[]; } +const PAGE_SIZE = 100; + export default class EditMembers extends React.PureComponent<Props, State> { mounted = false; - state: State = { loading: true, users: [], selectedUsers: [] }; + + constructor(props: Props) { + super(props); + + this.state = { + lastSearchParams: { + name: props.group.name, + organization: props.organization, + page: 1, + pageSize: PAGE_SIZE, + query: '', + selected: Filter.Selected + }, + listHasBeenTouched: false, + loading: true, + users: [], + selectedUsers: [] + }; + } componentDidMount() { this.mounted = true; - this.handleSearch('', Filter.Selected); + this.fetchUsers(this.state.lastSearchParams); } componentWillUnmount() { this.mounted = false; } - handleSearch = (query: string, selected: Filter) => { - return getUsersInGroup({ - name: this.props.group.name, - organization: this.props.organization, - ps: 100, - q: query !== '' ? query : undefined, - selected + fetchUsers = (searchParams: SearchParams, more?: boolean) => + getUsersInGroup({ + ...searchParams, + p: searchParams.page, + ps: searchParams.pageSize, + q: searchParams.query !== '' ? searchParams.query : undefined }).then( data => { if (this.mounted) { - this.setState({ - loading: false, - users: data.users, - selectedUsers: data.users.filter(user => user.selected).map(user => user.login) + this.setState(prevState => { + const users = more ? [...prevState.users, ...data.users] : data.users; + const newSelectedUsers = data.users + .filter(user => user.selected) + .map(user => user.login); + const selectedUsers = more + ? [...prevState.selectedUsers, ...newSelectedUsers] + : newSelectedUsers; + + return { + lastSearchParams: searchParams, + listHasBeenTouched: false, + loading: false, + users, + usersTotalCount: data.total, + selectedUsers + }; }); } }, @@ -79,35 +123,57 @@ export default class EditMembers extends React.PureComponent<Props, State> { } } ); - }; - handleSelect = (login: string) => { - return addUserToGroup({ + handleLoadMore = () => + this.fetchUsers( + { + ...this.state.lastSearchParams, + page: this.state.lastSearchParams.page + 1 + }, + true + ); + + handleReload = () => + this.fetchUsers({ + ...this.state.lastSearchParams, + page: 1 + }); + + handleSearch = (query: string, selected: Filter) => + this.fetchUsers({ + ...this.state.lastSearchParams, + page: 1, + query, + selected + }); + + handleSelect = (login: string) => + addUserToGroup({ name: this.props.group.name, login, organization: this.props.organization }).then(() => { if (this.mounted) { this.setState((state: State) => ({ + listHasBeenTouched: true, selectedUsers: [...state.selectedUsers, login] })); } }); - }; - handleUnselect = (login: string) => { - return removeUserFromGroup({ + handleUnselect = (login: string) => + removeUserFromGroup({ name: this.props.group.name, login, organization: this.props.organization }).then(() => { if (this.mounted) { this.setState((state: State) => ({ + listHasBeenTouched: true, selectedUsers: without(state.selectedUsers, login) })); } }); - }; renderElement = (login: string): React.ReactNode => { const user = find(this.state.users, { login }); @@ -138,6 +204,12 @@ export default class EditMembers extends React.PureComponent<Props, State> { <DeferredSpinner loading={this.state.loading}> <SelectList elements={this.state.users.map(user => user.login)} + elementsTotalCount={this.state.usersTotalCount} + needReload={ + this.state.listHasBeenTouched && this.state.lastSearchParams.selected !== Filter.All + } + onLoadMore={this.handleLoadMore} + onReload={this.handleReload} onSearch={this.handleSearch} onSelect={this.handleSelect} onUnselect={this.handleUnselect} diff --git a/server/sonar-web/src/main/js/apps/groups/components/__tests__/EditMembers-test.tsx b/server/sonar-web/src/main/js/apps/groups/components/__tests__/EditMembers-test.tsx index 1331cff110c..82505385a2c 100644 --- a/server/sonar-web/src/main/js/apps/groups/components/__tests__/EditMembers-test.tsx +++ b/server/sonar-web/src/main/js/apps/groups/components/__tests__/EditMembers-test.tsx @@ -36,6 +36,7 @@ it('should edit members', async () => { expect(wrapper).toMatchSnapshot(); await waitAndUpdate(wrapper); + click(wrapper.find('ResetButtonLink')); expect(onEdit).toBeCalled(); expect(wrapper).toMatchSnapshot(); diff --git a/server/sonar-web/src/main/js/apps/groups/components/__tests__/EditMembersModal-test.tsx b/server/sonar-web/src/main/js/apps/groups/components/__tests__/EditMembersModal-test.tsx index 6ee23a8ecb9..88209b8e099 100644 --- a/server/sonar-web/src/main/js/apps/groups/components/__tests__/EditMembersModal-test.tsx +++ b/server/sonar-web/src/main/js/apps/groups/components/__tests__/EditMembersModal-test.tsx @@ -17,15 +17,16 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -/* eslint-disable import/first, import/order */ -import * as React from 'react'; import { shallow } from 'enzyme'; -import EditMembersModal from '../EditMembersModal'; +import * as React from 'react'; +import EditMembersModal, { SearchParams } from '../EditMembersModal'; +import SelectList, { Filter } from '../../../../components/SelectList/SelectList'; import { waitAndUpdate } from '../../../../helpers/testUtils'; +import { getUsersInGroup, addUserToGroup, removeUserFromGroup } from '../../../../api/user_groups'; jest.mock('../../../../api/user_groups', () => ({ getUsersInGroup: jest.fn().mockResolvedValue({ - paging: { pageIndex: 0, pageSize: 10, total: 0 }, + paging: { pageIndex: 1, pageSize: 10, total: 1 }, users: [ { login: 'foo', @@ -33,20 +34,109 @@ jest.mock('../../../../api/user_groups', () => ({ selected: true } ] - }) + }), + addUserToGroup: jest.fn().mockResolvedValue({}), + removeUserFromGroup: jest.fn().mockResolvedValue({}) })); -const getUsersInGroup = require('../../../../api/user_groups').getUsersInGroup as jest.Mock<any>; - -const group = { id: 1, name: 'foo', membersCount: 1 }; +beforeEach(() => { + jest.clearAllMocks(); +}); -it('should render modal', async () => { - getUsersInGroup.mockClear(); +it('should render modal properly', async () => { + const wrapper = shallowRender(); + await waitAndUpdate(wrapper); - const wrapper = shallow(<EditMembersModal group={group} onClose={() => {}} organization="bar" />); expect(wrapper).toMatchSnapshot(); + expect(getUsersInGroup).toHaveBeenCalledWith( + expect.objectContaining({ + p: 1 + }) + ); + + wrapper.setState({ listHasBeenTouched: true }); + expect(wrapper.find(SelectList).props().needReload).toBe(true); + + wrapper.setState({ lastSearchParams: { selected: Filter.All } as SearchParams }); + expect(wrapper.find(SelectList).props().needReload).toBe(false); +}); +it('should handle reload properly', async () => { + const wrapper = shallowRender(); await waitAndUpdate(wrapper); - expect(getUsersInGroup).toHaveBeenCalledTimes(1); - expect(wrapper).toMatchSnapshot(); + + wrapper.instance().handleReload(); + expect(getUsersInGroup).toHaveBeenCalledWith( + expect.objectContaining({ + p: 1 + }) + ); + expect(wrapper.state().listHasBeenTouched).toBe(false); }); + +it('should handle search reload properly', async () => { + const wrapper = shallowRender(); + await waitAndUpdate(wrapper); + + wrapper.instance().handleSearch('foo', Filter.Selected); + expect(getUsersInGroup).toHaveBeenCalledWith( + expect.objectContaining({ + p: 1, + q: 'foo', + selected: Filter.Selected + }) + ); + expect(wrapper.state().listHasBeenTouched).toBe(false); +}); + +it('should handle load more properly', async () => { + const wrapper = shallowRender(); + await waitAndUpdate(wrapper); + + wrapper.instance().handleLoadMore(); + expect(getUsersInGroup).toHaveBeenCalledWith( + expect.objectContaining({ + p: 2 + }) + ); + expect(wrapper.state().listHasBeenTouched).toBe(false); +}); + +it('should handle selection properly', async () => { + const wrapper = shallowRender(); + await waitAndUpdate(wrapper); + + wrapper.instance().handleSelect('toto'); + await waitAndUpdate(wrapper); + expect(addUserToGroup).toHaveBeenCalledWith( + expect.objectContaining({ + login: 'toto' + }) + ); + expect(wrapper.state().listHasBeenTouched).toBe(true); +}); + +it('should handle deselection properly', async () => { + const wrapper = shallowRender(); + await waitAndUpdate(wrapper); + + wrapper.instance().handleUnselect('tata'); + await waitAndUpdate(wrapper); + expect(removeUserFromGroup).toHaveBeenCalledWith( + expect.objectContaining({ + login: 'tata' + }) + ); + expect(wrapper.state().listHasBeenTouched).toBe(true); +}); + +function shallowRender(props: Partial<EditMembersModal['props']> = {}) { + return shallow<EditMembersModal>( + <EditMembersModal + group={{ id: 1, name: 'foo', membersCount: 1 }} + onClose={jest.fn()} + organization={'bar'} + {...props} + /> + ); +} diff --git a/server/sonar-web/src/main/js/apps/groups/components/__tests__/__snapshots__/EditMembers-test.tsx.snap b/server/sonar-web/src/main/js/apps/groups/components/__tests__/__snapshots__/EditMembers-test.tsx.snap index 98241e10f0c..a2651397aff 100644 --- a/server/sonar-web/src/main/js/apps/groups/components/__tests__/__snapshots__/EditMembers-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/groups/components/__tests__/__snapshots__/EditMembers-test.tsx.snap @@ -311,6 +311,9 @@ exports[`should edit members 2`] = ` > <SelectList elements={Array []} + needReload={false} + onLoadMore={[Function]} + onReload={[Function]} onSearch={[Function]} onSelect={[Function]} onUnselect={[Function]} diff --git a/server/sonar-web/src/main/js/apps/groups/components/__tests__/__snapshots__/EditMembersModal-test.tsx.snap b/server/sonar-web/src/main/js/apps/groups/components/__tests__/__snapshots__/EditMembersModal-test.tsx.snap index b9156bda237..faff753d49b 100644 --- a/server/sonar-web/src/main/js/apps/groups/components/__tests__/__snapshots__/EditMembersModal-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/groups/components/__tests__/__snapshots__/EditMembersModal-test.tsx.snap @@ -1,50 +1,9 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`should render modal 1`] = ` +exports[`should render modal properly 1`] = ` <Modal contentLabel="users.update" - onRequestClose={[Function]} -> - <header - className="modal-head" - > - <h2> - users.update - </h2> - </header> - <div - className="modal-body" - > - <DeferredSpinner - loading={true} - timeout={100} - > - <SelectList - elements={Array []} - onSearch={[Function]} - onSelect={[Function]} - onUnselect={[Function]} - renderElement={[Function]} - selectedElements={Array []} - /> - </DeferredSpinner> - </div> - <footer - className="modal-foot" - > - <ResetButtonLink - onClick={[Function]} - > - Done - </ResetButtonLink> - </footer> -</Modal> -`; - -exports[`should render modal 2`] = ` -<Modal - contentLabel="users.update" - onRequestClose={[Function]} + onRequestClose={[MockFunction]} > <header className="modal-head" @@ -66,6 +25,9 @@ exports[`should render modal 2`] = ` "foo", ] } + needReload={false} + onLoadMore={[Function]} + onReload={[Function]} onSearch={[Function]} onSelect={[Function]} onUnselect={[Function]} @@ -82,7 +44,7 @@ exports[`should render modal 2`] = ` className="modal-foot" > <ResetButtonLink - onClick={[Function]} + onClick={[MockFunction]} > Done </ResetButtonLink> diff --git a/server/sonar-web/src/main/js/apps/organizationMembers/ManageMemberGroupsForm.tsx b/server/sonar-web/src/main/js/apps/organizationMembers/ManageMemberGroupsForm.tsx index fbc68ce2d8f..4355c2ab882 100644 --- a/server/sonar-web/src/main/js/apps/organizationMembers/ManageMemberGroupsForm.tsx +++ b/server/sonar-web/src/main/js/apps/organizationMembers/ManageMemberGroupsForm.tsx @@ -58,7 +58,10 @@ export default class ManageMemberGroupsForm extends React.PureComponent<Props, S loadUserGroups = () => { this.setState({ loading: true }); - getUserGroups(this.props.member.login, this.props.organization.key).then( + getUserGroups({ + login: this.props.member.login, + organization: this.props.organization.key + }).then( response => { if (this.mounted) { this.setState({ loading: false, userGroups: keyBy(response.groups, 'name') }); diff --git a/server/sonar-web/src/main/js/apps/users/components/GroupsForm.tsx b/server/sonar-web/src/main/js/apps/users/components/GroupsForm.tsx index 3e4b4554f43..b1cbfbd4f69 100644 --- a/server/sonar-web/src/main/js/apps/users/components/GroupsForm.tsx +++ b/server/sonar-web/src/main/js/apps/users/components/GroupsForm.tsx @@ -17,13 +17,13 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import * as React from 'react'; import { find, without } from 'lodash'; +import * as React from 'react'; +import { getUserGroups, UserGroup } from '../../../api/users'; +import { addUserToGroup, removeUserFromGroup } from '../../../api/user_groups'; import Modal from '../../../components/controls/Modal'; import SelectList, { Filter } from '../../../components/SelectList/SelectList'; import { translate } from '../../../helpers/l10n'; -import { getUserGroups, UserGroup } from '../../../api/users'; -import { addUserToGroup, removeUserFromGroup } from '../../../api/user_groups'; interface Props { onClose: () => void; @@ -31,47 +31,130 @@ interface Props { user: T.User; } +export interface SearchParams { + login: string; + organization?: string; + page: number; + pageSize: number; + query?: string; + selected: string; +} + interface State { groups: UserGroup[]; + groupsTotalCount?: number; + lastSearchParams: SearchParams; + listHasBeenTouched: boolean; selectedGroups: string[]; } -export default class GroupsForm extends React.PureComponent<Props> { - container?: HTMLDivElement | null; - state: State = { groups: [], selectedGroups: [] }; +const PAGE_SIZE = 100; + +export default class GroupsForm extends React.PureComponent<Props, State> { + mounted = false; + + constructor(props: Props) { + super(props); + + this.state = { + groups: [], + lastSearchParams: { + login: props.user.login, + page: 1, + pageSize: PAGE_SIZE, + query: '', + selected: Filter.Selected + }, + listHasBeenTouched: false, + selectedGroups: [] + }; + } componentDidMount() { - this.handleSearch('', Filter.Selected); + this.mounted = true; + this.fetchUsers(this.state.lastSearchParams); } - handleSearch = (query: string, selected: Filter) => { - return getUserGroups(this.props.user.login, undefined, query, selected).then(data => { - this.setState({ - groups: data.groups, - selectedGroups: data.groups.filter(group => group.selected).map(group => group.name) - }); + componentWillUnmount() { + this.mounted = false; + } + + fetchUsers = (searchParams: SearchParams, more?: boolean) => + getUserGroups({ + login: searchParams.login, + organization: searchParams.organization !== '' ? searchParams.organization : undefined, + p: searchParams.page, + ps: searchParams.pageSize, + q: searchParams.query !== '' ? searchParams.query : undefined, + selected: searchParams.selected + }).then(data => { + if (this.mounted) { + this.setState(prevState => { + const groups = more ? [...prevState.groups, ...data.groups] : data.groups; + const newSeletedGroups = data.groups.filter(gp => gp.selected).map(gp => gp.name); + const selectedGroups = more + ? [...prevState.selectedGroups, ...newSeletedGroups] + : newSeletedGroups; + + return { + lastSearchParams: searchParams, + listHasBeenTouched: false, + groups, + groupsTotalCount: data.paging.total, + selectedGroups + }; + }); + } }); - }; - handleSelect = (name: string) => { - return addUserToGroup({ + handleLoadMore = () => + this.fetchUsers( + { + ...this.state.lastSearchParams, + page: this.state.lastSearchParams.page + 1 + }, + true + ); + + handleReload = () => + this.fetchUsers({ + ...this.state.lastSearchParams, + page: 1 + }); + + handleSearch = (query: string, selected: Filter) => + this.fetchUsers({ + ...this.state.lastSearchParams, + page: 1, + query, + selected + }); + + handleSelect = (name: string) => + addUserToGroup({ name, login: this.props.user.login }).then(() => { - this.setState((state: State) => ({ selectedGroups: [...state.selectedGroups, name] })); + if (this.mounted) { + this.setState((state: State) => ({ + listHasBeenTouched: true, + selectedGroups: [...state.selectedGroups, name] + })); + } }); - }; - handleUnselect = (name: string) => { - return removeUserFromGroup({ + handleUnselect = (name: string) => + removeUserFromGroup({ name, login: this.props.user.login }).then(() => { - this.setState((state: State) => ({ - selectedGroups: without(state.selectedGroups, name) - })); + if (this.mounted) { + this.setState((state: State) => ({ + listHasBeenTouched: true, + selectedGroups: without(state.selectedGroups, name) + })); + } }); - }; handleCloseClick = (event: React.SyntheticEvent<HTMLElement>) => { event.preventDefault(); @@ -112,6 +195,12 @@ export default class GroupsForm extends React.PureComponent<Props> { <div className="modal-body"> <SelectList elements={this.state.groups.map(group => group.name)} + elementsTotalCount={this.state.groupsTotalCount} + needReload={ + this.state.listHasBeenTouched && this.state.lastSearchParams.selected !== Filter.All + } + onLoadMore={this.handleLoadMore} + onReload={this.handleReload} onSearch={this.handleSearch} onSelect={this.handleSelect} onUnselect={this.handleUnselect} diff --git a/server/sonar-web/src/main/js/apps/users/components/__tests__/GroupsForm-test.tsx b/server/sonar-web/src/main/js/apps/users/components/__tests__/GroupsForm-test.tsx new file mode 100644 index 00000000000..6c86567a743 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/users/components/__tests__/GroupsForm-test.tsx @@ -0,0 +1,155 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 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 GroupsForm, { SearchParams } from '../GroupsForm'; +import SelectList, { Filter } from '../../../../components/SelectList/SelectList'; +import { waitAndUpdate } from '../../../../helpers/testUtils'; +import { getUserGroups } from '../../../../api/users'; +import { addUserToGroup, removeUserFromGroup } from '../../../../api/user_groups'; +import { mockUser } from '../../../../helpers/testMocks'; + +jest.mock('../../../../api/users', () => ({ + getUserGroups: jest.fn().mockResolvedValue({ + paging: { pageIndex: 1, pageSize: 10, total: 1 }, + groups: [ + { + id: 1001, + name: 'test1', + description: 'test1', + selected: true + }, + { + id: 1002, + name: 'test2', + description: 'test2', + selected: true + }, + { + id: 1003, + name: 'test3', + description: 'test3', + selected: false + } + ] + }) +})); + +jest.mock('../../../../api/user_groups', () => ({ + addUserToGroup: jest.fn().mockResolvedValue({}), + removeUserFromGroup: jest.fn().mockResolvedValue({}) +})); + +beforeEach(() => { + jest.clearAllMocks(); +}); + +it('should render correctly', async () => { + const wrapper = shallowRender(); + await waitAndUpdate(wrapper); + + expect(wrapper).toMatchSnapshot(); + expect(getUserGroups).toHaveBeenCalledWith( + expect.objectContaining({ + p: 1 + }) + ); + + wrapper.setState({ listHasBeenTouched: true }); + expect(wrapper.find(SelectList).props().needReload).toBe(true); + + wrapper.setState({ lastSearchParams: { selected: Filter.All } as SearchParams }); + expect(wrapper.find(SelectList).props().needReload).toBe(false); +}); + +it('should handle reload properly', async () => { + const wrapper = shallowRender(); + await waitAndUpdate(wrapper); + + wrapper.instance().handleReload(); + expect(getUserGroups).toHaveBeenCalledWith( + expect.objectContaining({ + p: 1 + }) + ); + expect(wrapper.state().listHasBeenTouched).toBe(false); +}); + +it('should handle search reload properly', async () => { + const wrapper = shallowRender(); + await waitAndUpdate(wrapper); + + wrapper.instance().handleSearch('foo', Filter.Selected); + expect(getUserGroups).toHaveBeenCalledWith( + expect.objectContaining({ + p: 1, + q: 'foo', + selected: Filter.Selected + }) + ); + expect(wrapper.state().listHasBeenTouched).toBe(false); +}); + +it('should handle load more properly', async () => { + const wrapper = shallowRender(); + await waitAndUpdate(wrapper); + + wrapper.instance().handleLoadMore(); + expect(getUserGroups).toHaveBeenCalledWith( + expect.objectContaining({ + p: 2 + }) + ); + expect(wrapper.state().listHasBeenTouched).toBe(false); +}); + +it('should handle selection properly', async () => { + const wrapper = shallowRender(); + await waitAndUpdate(wrapper); + + wrapper.instance().handleSelect('toto'); + await waitAndUpdate(wrapper); + expect(addUserToGroup).toHaveBeenCalledWith( + expect.objectContaining({ + name: 'toto' + }) + ); + expect(wrapper.state().listHasBeenTouched).toBe(true); +}); + +it('should handle deselection properly', async () => { + const wrapper = shallowRender(); + await waitAndUpdate(wrapper); + + wrapper.instance().handleUnselect('tata'); + await waitAndUpdate(wrapper); + expect(removeUserFromGroup).toHaveBeenCalledWith( + expect.objectContaining({ + name: 'tata' + }) + ); + expect(wrapper.state().listHasBeenTouched).toBe(true); +}); + +function shallowRender(props: Partial<GroupsForm['props']> = {}) { + return shallow<GroupsForm>( + <GroupsForm onClose={jest.fn()} onUpdateUsers={jest.fn()} user={mockUser()} {...props} /> + ); +} diff --git a/server/sonar-web/src/main/js/apps/users/components/__tests__/__snapshots__/GroupsForm-test.tsx.snap b/server/sonar-web/src/main/js/apps/users/components/__tests__/__snapshots__/GroupsForm-test.tsx.snap new file mode 100644 index 00000000000..a544c913d55 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/users/components/__tests__/__snapshots__/GroupsForm-test.tsx.snap @@ -0,0 +1,54 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should render correctly 1`] = ` +<Modal + contentLabel="users.update_groups" + onRequestClose={[Function]} +> + <div + className="modal-head" + > + <h2> + users.update_groups + </h2> + </div> + <div + className="modal-body" + > + <SelectList + elements={ + Array [ + "test1", + "test2", + "test3", + ] + } + elementsTotalCount={1} + needReload={false} + onLoadMore={[Function]} + onReload={[Function]} + onSearch={[Function]} + onSelect={[Function]} + onUnselect={[Function]} + renderElement={[Function]} + selectedElements={ + Array [ + "test1", + "test2", + ] + } + /> + </div> + <footer + className="modal-foot" + > + <a + className="js-modal-close" + href="#" + onClick={[Function]} + > + Done + </a> + </footer> +</Modal> +`; |