diff --git a/x-pack/plugins/enterprise_search/public/applications/__mocks__/react_router_history.mock.ts b/x-pack/plugins/enterprise_search/public/applications/__mocks__/react_router_history.mock.ts index 2c833bcfeaf4c..1516aa9096eca 100644 --- a/x-pack/plugins/enterprise_search/public/applications/__mocks__/react_router_history.mock.ts +++ b/x-pack/plugins/enterprise_search/public/applications/__mocks__/react_router_history.mock.ts @@ -28,6 +28,7 @@ jest.mock('react-router-dom', () => ({ ...(jest.requireActual('react-router-dom') as object), useHistory: jest.fn(() => mockHistory), useLocation: jest.fn(() => mockLocation), + useParams: jest.fn(() => ({})), })); /** diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/use_did_update_effect/index.ts b/x-pack/plugins/enterprise_search/public/applications/shared/use_did_update_effect/index.ts deleted file mode 100644 index 05c60ebced088..0000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/shared/use_did_update_effect/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -export { useDidUpdateEffect } from './use_did_update_effect'; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/use_did_update_effect/use_did_update_effect.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/use_did_update_effect/use_did_update_effect.test.tsx deleted file mode 100644 index e3d2ffb44f01e..0000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/shared/use_did_update_effect/use_did_update_effect.test.tsx +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React, { useState } from 'react'; -import { mount } from 'enzyme'; - -import { EuiLink } from '@elastic/eui'; - -import { useDidUpdateEffect } from './use_did_update_effect'; - -const fn = jest.fn(); - -const TestHook = ({ value }: { value: number }) => { - const [inputValue, setValue] = useState(value); - useDidUpdateEffect(fn, [inputValue]); - return setValue(2)} />; -}; - -const wrapper = mount(); - -describe('useDidUpdateEffect', () => { - it('should not fire function when value unchanged', () => { - expect(fn).not.toHaveBeenCalled(); - }); - - it('should fire function when value changed', () => { - wrapper.find(EuiLink).simulate('click'); - expect(fn).toHaveBeenCalled(); - }); -}); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/use_did_update_effect/use_did_update_effect.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/use_did_update_effect/use_did_update_effect.tsx deleted file mode 100644 index 4c3e10fc84b84..0000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/shared/use_did_update_effect/use_did_update_effect.tsx +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -/* - * Sometimes we don't want to fire the initial useEffect call. - * This custom Hook only fires after the intial render has completed. - */ -import { useEffect, useRef, DependencyList } from 'react'; - -export const useDidUpdateEffect = (fn: Function, inputs: DependencyList) => { - const didMountRef = useRef(false); - - useEffect(() => { - if (didMountRef.current) { - fn(); - } else { - didMountRef.current = true; - } - }, inputs); -}; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/__mocks__/content_sources.mock.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/__mocks__/content_sources.mock.ts index 7789d0caba345..c305ae9d5f7a9 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/__mocks__/content_sources.mock.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/__mocks__/content_sources.mock.ts @@ -20,7 +20,7 @@ export const contentSources = [ boost: 1, }, { - id: '123', + id: '124', serviceType: 'jira', searchable: true, supportedByLicense: true, diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/__mocks__/group_logic.mock.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/__mocks__/group_logic.mock.ts new file mode 100644 index 0000000000000..18e7851485222 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/__mocks__/group_logic.mock.ts @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { IGroupValues } from '../group_logic'; + +import { IGroupDetails, ISourcePriority } from '../../../types'; + +export const mockGroupValues = { + group: {} as IGroupDetails, + dataLoading: true, + manageUsersModalVisible: false, + managerModalFormErrors: [], + sharedSourcesModalVisible: false, + confirmDeleteModalVisible: false, + groupNameInputValue: '', + selectedGroupSources: [], + selectedGroupUsers: [], + groupPrioritiesUnchanged: true, + activeSourcePriorities: {} as ISourcePriority, + cachedSourcePriorities: {} as ISourcePriority, +} as IGroupValues; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/__mocks__/groups_logic.mock.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/__mocks__/groups_logic.mock.ts new file mode 100644 index 0000000000000..0483799b73093 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/__mocks__/groups_logic.mock.ts @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { IGroupsValues } from '../groups_logic'; + +import { IContentSource, IUser, IGroup } from '../../../types'; + +import { DEFAULT_META } from '../../../../shared/constants'; + +export const mockGroupsValues = { + groups: [] as IGroup[], + contentSources: [] as IContentSource[], + users: [] as IUser[], + groupsDataLoading: true, + groupListLoading: true, + newGroupModalOpen: false, + newGroupName: '', + hasFiltersSet: false, + newGroup: null, + newGroupNameErrors: [], + filterSourcesDropdownOpen: false, + filteredSources: [], + filterUsersDropdownOpen: false, + filteredUsers: [], + allGroupUsersLoading: false, + allGroupUsers: [], + filterValue: '', + groupsMeta: DEFAULT_META, +} as IGroupsValues; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/group_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/group_logic.test.ts new file mode 100644 index 0000000000000..b4724c8ed3f0e --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/group_logic.test.ts @@ -0,0 +1,509 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { resetContext } from 'kea'; + +jest.mock('../../../shared/http', () => ({ + HttpLogic: { + values: { http: { get: jest.fn(), post: jest.fn(), put: jest.fn(), delete: jest.fn() } }, + }, +})); +import { HttpLogic } from '../../../shared/http'; + +jest.mock('../../../shared/flash_messages', () => ({ + FlashMessagesLogic: { actions: { clearFlashMessages: jest.fn(), setQueuedMessages: jest.fn() } }, + flashAPIErrors: jest.fn(), + setSuccessMessage: jest.fn(), + setQueuedSuccessMessage: jest.fn(), +})); +import { + FlashMessagesLogic, + flashAPIErrors, + setSuccessMessage, + setQueuedSuccessMessage, +} from '../../../shared/flash_messages'; + +jest.mock('../../../shared/kibana', () => ({ + KibanaLogic: { values: { navigateToUrl: jest.fn() } }, +})); +import { KibanaLogic } from '../../../shared/kibana'; + +import { groups } from '../../__mocks__/groups.mock'; +import { mockGroupValues } from './__mocks__/group_logic.mock'; +import { GroupLogic } from './group_logic'; + +import { GROUPS_PATH } from '../../routes'; + +describe('GroupLogic', () => { + const group = groups[0]; + const sourceIds = ['123', '124']; + const userIds = ['1z1z']; + const sourcePriorities = { [sourceIds[0]]: 1, [sourceIds[1]]: 0.5 }; + const clearFlashMessagesSpy = jest.spyOn(FlashMessagesLogic.actions, 'clearFlashMessages'); + + beforeEach(() => { + jest.clearAllMocks(); + resetContext({}); + GroupLogic.mount(); + }); + + it('has expected default values', () => { + expect(GroupLogic.values).toEqual(mockGroupValues); + }); + + describe('actions', () => { + describe('onInitializeGroup', () => { + it('sets reducers', () => { + GroupLogic.actions.onInitializeGroup(group); + + expect(GroupLogic.values.group).toEqual(group); + expect(GroupLogic.values.dataLoading).toEqual(false); + expect(GroupLogic.values.groupNameInputValue).toEqual(group.name); + expect(GroupLogic.values.selectedGroupSources).toEqual(sourceIds); + expect(GroupLogic.values.selectedGroupUsers).toEqual(userIds); + expect(GroupLogic.values.cachedSourcePriorities).toEqual(sourcePriorities); + expect(GroupLogic.values.activeSourcePriorities).toEqual(sourcePriorities); + expect(GroupLogic.values.groupPrioritiesUnchanged).toEqual(true); + }); + }); + + describe('onGroupNameChanged', () => { + it('sets reducers', () => { + const renamedGroup = { + ...group, + name: 'changed', + }; + GroupLogic.actions.onGroupNameChanged(renamedGroup); + + expect(GroupLogic.values.group).toEqual(renamedGroup); + expect(GroupLogic.values.groupNameInputValue).toEqual(renamedGroup.name); + }); + }); + + describe('onGroupPrioritiesChanged', () => { + it('sets reducers', () => { + GroupLogic.actions.onGroupPrioritiesChanged(group); + + expect(GroupLogic.values.dataLoading).toEqual(false); + expect(GroupLogic.values.cachedSourcePriorities).toEqual(sourcePriorities); + expect(GroupLogic.values.activeSourcePriorities).toEqual(sourcePriorities); + }); + }); + + describe('onGroupNameInputChange', () => { + it('sets reducers', () => { + const name = 'new name'; + GroupLogic.actions.onGroupNameInputChange(name); + + expect(GroupLogic.values.groupNameInputValue).toEqual(name); + }); + }); + + describe('addGroupSource', () => { + it('sets reducer', () => { + GroupLogic.actions.addGroupSource(sourceIds[0]); + + expect(GroupLogic.values.selectedGroupSources).toEqual([sourceIds[0]]); + }); + }); + + describe('removeGroupSource', () => { + it('sets reducers', () => { + GroupLogic.actions.addGroupSource(sourceIds[0]); + GroupLogic.actions.addGroupSource(sourceIds[1]); + GroupLogic.actions.removeGroupSource(sourceIds[0]); + + expect(GroupLogic.values.selectedGroupSources).toEqual([sourceIds[1]]); + }); + }); + + describe('addGroupUser', () => { + it('sets reducer', () => { + GroupLogic.actions.addGroupUser(sourceIds[0]); + + expect(GroupLogic.values.selectedGroupUsers).toEqual([sourceIds[0]]); + }); + }); + + describe('removeGroupUser', () => { + it('sets reducers', () => { + GroupLogic.actions.addGroupUser(sourceIds[0]); + GroupLogic.actions.addGroupUser(sourceIds[1]); + GroupLogic.actions.removeGroupUser(sourceIds[0]); + + expect(GroupLogic.values.selectedGroupUsers).toEqual([sourceIds[1]]); + }); + }); + + describe('onGroupSourcesSaved', () => { + it('sets reducers', () => { + GroupLogic.actions.onGroupSourcesSaved(group); + + expect(GroupLogic.values.group).toEqual(group); + expect(GroupLogic.values.sharedSourcesModalVisible).toEqual(false); + expect(GroupLogic.values.selectedGroupSources).toEqual(sourceIds); + expect(GroupLogic.values.cachedSourcePriorities).toEqual(sourcePriorities); + expect(GroupLogic.values.activeSourcePriorities).toEqual(sourcePriorities); + }); + }); + + describe('onGroupUsersSaved', () => { + it('sets reducers', () => { + GroupLogic.actions.onGroupUsersSaved(group); + + expect(GroupLogic.values.group).toEqual(group); + expect(GroupLogic.values.manageUsersModalVisible).toEqual(false); + expect(GroupLogic.values.selectedGroupUsers).toEqual(userIds); + }); + }); + + describe('setGroupModalErrors', () => { + it('sets reducers', () => { + const errors = ['this is an error']; + GroupLogic.actions.setGroupModalErrors(errors); + + expect(GroupLogic.values.managerModalFormErrors).toEqual(errors); + }); + }); + + describe('hideSharedSourcesModal', () => { + it('sets reducers', () => { + GroupLogic.actions.hideSharedSourcesModal(group); + + expect(GroupLogic.values.sharedSourcesModalVisible).toEqual(false); + expect(GroupLogic.values.selectedGroupSources).toEqual(sourceIds); + }); + }); + + describe('hideManageUsersModal', () => { + it('sets reducers', () => { + GroupLogic.actions.hideManageUsersModal(group); + + expect(GroupLogic.values.manageUsersModalVisible).toEqual(false); + expect(GroupLogic.values.managerModalFormErrors).toEqual([]); + expect(GroupLogic.values.selectedGroupUsers).toEqual(userIds); + }); + }); + + describe('selectAllSources', () => { + it('sets reducers', () => { + GroupLogic.actions.selectAllSources(group.contentSources); + + expect(GroupLogic.values.selectedGroupSources).toEqual(sourceIds); + }); + }); + + describe('selectAllUsers', () => { + it('sets reducers', () => { + GroupLogic.actions.selectAllUsers(group.users); + + expect(GroupLogic.values.selectedGroupUsers).toEqual(userIds); + }); + }); + + describe('updatePriority', () => { + it('sets reducers', () => { + const PRIORITY_VALUE = 4; + GroupLogic.actions.updatePriority(sourceIds[0], PRIORITY_VALUE); + + expect(GroupLogic.values.activeSourcePriorities).toEqual({ + [sourceIds[0]]: PRIORITY_VALUE, + }); + expect(GroupLogic.values.groupPrioritiesUnchanged).toEqual(false); + }); + }); + + describe('resetGroup', () => { + it('sets reducers', () => { + GroupLogic.actions.resetGroup(); + + expect(GroupLogic.values.group).toEqual({}); + expect(GroupLogic.values.dataLoading).toEqual(true); + }); + }); + + describe('hideConfirmDeleteModal', () => { + it('sets reducer', () => { + GroupLogic.actions.showConfirmDeleteModal(); + GroupLogic.actions.hideConfirmDeleteModal(); + + expect(GroupLogic.values.confirmDeleteModalVisible).toEqual(false); + }); + }); + }); + + describe('listeners', () => { + describe('initializeGroup', () => { + it('calls API and sets values', async () => { + const onInitializeGroupSpy = jest.spyOn(GroupLogic.actions, 'onInitializeGroup'); + const promise = Promise.resolve(group); + (HttpLogic.values.http.get as jest.Mock).mockReturnValue(promise); + + GroupLogic.actions.initializeGroup(sourceIds[0]); + expect(HttpLogic.values.http.get).toHaveBeenCalledWith('/api/workplace_search/groups/123'); + await promise; + expect(onInitializeGroupSpy).toHaveBeenCalledWith(group); + }); + + it('handles 404 error', async () => { + const promise = Promise.reject({ response: { status: 404 } }); + (HttpLogic.values.http.get as jest.Mock).mockReturnValue(promise); + + GroupLogic.actions.initializeGroup(sourceIds[0]); + + try { + await promise; + } catch { + expect(KibanaLogic.values.navigateToUrl).toHaveBeenCalledWith(GROUPS_PATH); + expect(FlashMessagesLogic.actions.setQueuedMessages).toHaveBeenCalledWith({ + type: 'error', + message: 'Unable to find group with ID: "123".', + }); + } + }); + + it('handles non-404 error', async () => { + const promise = Promise.reject('this is an error'); + (HttpLogic.values.http.get as jest.Mock).mockReturnValue(promise); + + GroupLogic.actions.initializeGroup(sourceIds[0]); + + try { + await promise; + } catch { + expect(KibanaLogic.values.navigateToUrl).toHaveBeenCalledWith(GROUPS_PATH); + expect(FlashMessagesLogic.actions.setQueuedMessages).toHaveBeenCalledWith({ + type: 'error', + message: 'this is an error', + }); + } + }); + }); + + describe('deleteGroup', () => { + beforeEach(() => { + GroupLogic.actions.onInitializeGroup(group); + }); + it('deletes a group', async () => { + const promise = Promise.resolve(true); + (HttpLogic.values.http.delete as jest.Mock).mockReturnValue(promise); + + GroupLogic.actions.deleteGroup(); + expect(HttpLogic.values.http.delete).toHaveBeenCalledWith( + '/api/workplace_search/groups/123' + ); + + await promise; + expect(KibanaLogic.values.navigateToUrl).toHaveBeenCalledWith(GROUPS_PATH); + expect(setQueuedSuccessMessage).toHaveBeenCalledWith( + 'Group "group" was successfully deleted.' + ); + }); + + it('handles error', async () => { + const promise = Promise.reject('this is an error'); + (HttpLogic.values.http.delete as jest.Mock).mockReturnValue(promise); + + GroupLogic.actions.deleteGroup(); + try { + await promise; + } catch { + expect(flashAPIErrors).toHaveBeenCalledWith('this is an error'); + } + }); + }); + + describe('updateGroupName', () => { + beforeEach(() => { + GroupLogic.actions.onInitializeGroup(group); + GroupLogic.actions.onGroupNameInputChange('new name'); + }); + it('updates name', async () => { + const onGroupNameChangedSpy = jest.spyOn(GroupLogic.actions, 'onGroupNameChanged'); + const promise = Promise.resolve(group); + (HttpLogic.values.http.put as jest.Mock).mockReturnValue(promise); + + GroupLogic.actions.updateGroupName(); + expect(HttpLogic.values.http.put).toHaveBeenCalledWith('/api/workplace_search/groups/123', { + body: JSON.stringify({ group: { name: 'new name' } }), + }); + + await promise; + expect(onGroupNameChangedSpy).toHaveBeenCalledWith(group); + expect(setSuccessMessage).toHaveBeenCalledWith( + 'Successfully renamed this group to "group".' + ); + }); + + it('handles error', async () => { + const promise = Promise.reject('this is an error'); + (HttpLogic.values.http.put as jest.Mock).mockReturnValue(promise); + + GroupLogic.actions.updateGroupName(); + try { + await promise; + } catch { + expect(flashAPIErrors).toHaveBeenCalledWith('this is an error'); + } + }); + }); + + describe('saveGroupSources', () => { + beforeEach(() => { + GroupLogic.actions.onInitializeGroup(group); + GroupLogic.actions.selectAllSources(group.contentSources); + }); + it('updates name', async () => { + const onGroupSourcesSavedSpy = jest.spyOn(GroupLogic.actions, 'onGroupSourcesSaved'); + const promise = Promise.resolve(group); + (HttpLogic.values.http.post as jest.Mock).mockReturnValue(promise); + + GroupLogic.actions.saveGroupSources(); + expect(HttpLogic.values.http.post).toHaveBeenCalledWith( + '/api/workplace_search/groups/123/share', + { + body: JSON.stringify({ content_source_ids: sourceIds }), + } + ); + + await promise; + expect(onGroupSourcesSavedSpy).toHaveBeenCalledWith(group); + expect(setSuccessMessage).toHaveBeenCalledWith( + 'Successfully updated shared content sources.' + ); + }); + + it('handles error', async () => { + const promise = Promise.reject('this is an error'); + (HttpLogic.values.http.post as jest.Mock).mockReturnValue(promise); + + GroupLogic.actions.saveGroupSources(); + try { + await promise; + } catch { + expect(flashAPIErrors).toHaveBeenCalledWith('this is an error'); + } + }); + }); + + describe('saveGroupUsers', () => { + beforeEach(() => { + GroupLogic.actions.onInitializeGroup(group); + }); + it('updates name', async () => { + const onGroupUsersSavedSpy = jest.spyOn(GroupLogic.actions, 'onGroupUsersSaved'); + const promise = Promise.resolve(group); + (HttpLogic.values.http.post as jest.Mock).mockReturnValue(promise); + + GroupLogic.actions.saveGroupUsers(); + expect(HttpLogic.values.http.post).toHaveBeenCalledWith( + '/api/workplace_search/groups/123/assign', + { + body: JSON.stringify({ user_ids: userIds }), + } + ); + + await promise; + expect(onGroupUsersSavedSpy).toHaveBeenCalledWith(group); + expect(setSuccessMessage).toHaveBeenCalledWith( + 'Successfully updated the users of this group.' + ); + }); + + it('handles error', async () => { + const promise = Promise.reject('this is an error'); + (HttpLogic.values.http.post as jest.Mock).mockReturnValue(promise); + + GroupLogic.actions.saveGroupUsers(); + try { + await promise; + } catch { + expect(flashAPIErrors).toHaveBeenCalledWith('this is an error'); + } + }); + }); + + describe('saveGroupSourcePrioritization', () => { + beforeEach(() => { + GroupLogic.actions.onInitializeGroup(group); + }); + it('updates name', async () => { + const onGroupPrioritiesChangedSpy = jest.spyOn( + GroupLogic.actions, + 'onGroupPrioritiesChanged' + ); + const promise = Promise.resolve(group); + (HttpLogic.values.http.put as jest.Mock).mockReturnValue(promise); + + GroupLogic.actions.saveGroupSourcePrioritization(); + expect(HttpLogic.values.http.put).toHaveBeenCalledWith( + '/api/workplace_search/groups/123/boosts', + { + body: JSON.stringify({ + content_source_boosts: [ + [sourceIds[0], 1], + [sourceIds[1], 0.5], + ], + }), + } + ); + + await promise; + expect(setSuccessMessage).toHaveBeenCalledWith( + 'Successfully updated shared source prioritization.' + ); + expect(onGroupPrioritiesChangedSpy).toHaveBeenCalledWith(group); + }); + + it('handles error', async () => { + const promise = Promise.reject('this is an error'); + (HttpLogic.values.http.put as jest.Mock).mockReturnValue(promise); + + GroupLogic.actions.saveGroupSourcePrioritization(); + try { + await promise; + } catch { + expect(flashAPIErrors).toHaveBeenCalledWith('this is an error'); + } + }); + }); + + describe('showConfirmDeleteModal', () => { + it('sets reducer and clears flash messages', () => { + GroupLogic.actions.showConfirmDeleteModal(); + + expect(GroupLogic.values.confirmDeleteModalVisible).toEqual(true); + expect(clearFlashMessagesSpy).toHaveBeenCalled(); + }); + }); + + describe('showSharedSourcesModal', () => { + it('sets reducer and clears flash messages', () => { + GroupLogic.actions.showSharedSourcesModal(); + + expect(GroupLogic.values.sharedSourcesModalVisible).toEqual(true); + expect(clearFlashMessagesSpy).toHaveBeenCalled(); + }); + }); + + describe('showManageUsersModal', () => { + it('sets reducer and clears flash messages', () => { + GroupLogic.actions.showManageUsersModal(); + + expect(GroupLogic.values.manageUsersModalVisible).toEqual(true); + expect(clearFlashMessagesSpy).toHaveBeenCalled(); + }); + }); + + describe('resetFlashMessages', () => { + it('clears flash messages', () => { + GroupLogic.actions.resetFlashMessages(); + + expect(clearFlashMessagesSpy).toHaveBeenCalled(); + }); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/group_logic.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/group_logic.ts index 1ce0fe53726d4..b895200d3fc22 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/group_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/group_logic.ts @@ -56,13 +56,11 @@ export interface IGroupActions { } export interface IGroupValues { - contentSources: IContentSourceDetails[]; - users: IUser[]; group: IGroupDetails; dataLoading: boolean; manageUsersModalVisible: boolean; managerModalFormErrors: string[]; - sharedSourcesModalModalVisible: boolean; + sharedSourcesModalVisible: boolean; confirmDeleteModalVisible: boolean; groupNameInputValue: string; selectedGroupSources: string[]; @@ -138,7 +136,7 @@ export const GroupLogic = kea>({ hideManageUsersModal: () => [], }, ], - sharedSourcesModalModalVisible: [ + sharedSourcesModalVisible: [ false, { showSharedSourcesModal: () => true, @@ -225,8 +223,7 @@ export const GroupLogic = kea>({ } ); - const error = e.response.status === 404 ? NOT_FOUND_MESSAGE : e; - + const error = e.response?.status === 404 ? NOT_FOUND_MESSAGE : e; FlashMessagesLogic.actions.setQueuedMessages({ type: 'error', message: error, @@ -321,7 +318,7 @@ export const GroupLogic = kea>({ const GROUP_USERS_UPDATED_MESSAGE = i18n.translate( 'xpack.enterpriseSearch.workplaceSearch.groups.groupUsersUpdated', { - defaultMessage: 'Successfully updated the users of this group', + defaultMessage: 'Successfully updated the users of this group.', } ); setSuccessMessage(GROUP_USERS_UPDATED_MESSAGE); @@ -353,7 +350,7 @@ export const GroupLogic = kea>({ const GROUP_PRIORITIZATION_UPDATED_MESSAGE = i18n.translate( 'xpack.enterpriseSearch.workplaceSearch.groups.groupPrioritizationUpdated', { - defaultMessage: 'Successfully updated shared source prioritization', + defaultMessage: 'Successfully updated shared source prioritization.', } ); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/group_router.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/group_router.test.tsx new file mode 100644 index 0000000000000..6f293920fa387 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/group_router.test.tsx @@ -0,0 +1,87 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import '../../../__mocks__/kea.mock'; +import '../../../__mocks__/shallow_useeffect.mock'; + +import { setMockValues, setMockActions } from '../../../__mocks__'; + +import React from 'react'; +import { shallow } from 'enzyme'; + +import { Route, Switch } from 'react-router-dom'; + +import { groups } from '../../__mocks__/groups.mock'; + +import { SetWorkplaceSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome'; + +import { GroupOverview } from './components/group_overview'; +import { GroupSourcePrioritization } from './components/group_source_prioritization'; + +import { GroupRouter } from './group_router'; + +import { FlashMessages } from '../../../shared/flash_messages'; + +import { ManageUsersModal } from './components/manage_users_modal'; +import { SharedSourcesModal } from './components/shared_sources_modal'; + +describe('GroupRouter', () => { + const initializeGroup = jest.fn(); + const resetGroup = jest.fn(); + + beforeEach(() => { + setMockValues({ + sharedSourcesModalVisible: false, + manageUsersModalVisible: false, + group: groups[0], + }); + + setMockActions({ + initializeGroup, + resetGroup, + }); + }); + it('renders', () => { + const wrapper = shallow(); + + expect(wrapper.find(FlashMessages)).toHaveLength(1); + expect(wrapper.find(Switch)).toHaveLength(1); + expect(wrapper.find(Route)).toHaveLength(2); + expect(wrapper.find(GroupOverview)).toHaveLength(1); + expect(wrapper.find(GroupSourcePrioritization)).toHaveLength(1); + }); + + it('renders modals', () => { + setMockValues({ + sharedSourcesModalVisible: true, + manageUsersModalVisible: true, + group: groups[0], + }); + + const wrapper = shallow(); + + expect(wrapper.find(ManageUsersModal)).toHaveLength(1); + expect(wrapper.find(SharedSourcesModal)).toHaveLength(1); + }); + + it('handles breadcrumbs while loading', () => { + setMockValues({ + sharedSourcesModalVisible: false, + manageUsersModalVisible: false, + group: {}, + }); + + const loadingBreadcrumbs = ['Groups', '...']; + + const wrapper = shallow(); + + const firstBreadCrumb = wrapper.find(SetPageChrome).first(); + const lastBreadCrumb = wrapper.find(SetPageChrome).last(); + + expect(firstBreadCrumb.prop('trail')).toEqual([...loadingBreadcrumbs, 'Source Prioritization']); + expect(lastBreadCrumb.prop('trail')).toEqual(loadingBreadcrumbs); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/group_router.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/group_router.tsx index 0a637497a5b05..1b6f0c4c49a05 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/group_router.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/group_router.tsx @@ -9,7 +9,7 @@ import React, { useEffect } from 'react'; import { useActions, useValues } from 'kea'; import { Route, Switch, useParams } from 'react-router-dom'; -import { FlashMessages, FlashMessagesLogic } from '../../../shared/flash_messages'; +import { FlashMessages } from '../../../shared/flash_messages'; import { SetWorkplaceSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome'; import { SendWorkplaceSearchTelemetry as SendTelemetry } from '../../../shared/telemetry'; @@ -26,16 +26,13 @@ import { GroupSourcePrioritization } from './components/group_source_prioritizat export const GroupRouter: React.FC = () => { const { groupId } = useParams() as { groupId: string }; - const { messages } = useValues(FlashMessagesLogic); const { initializeGroup, resetGroup } = useActions(GroupLogic); const { - sharedSourcesModalModalVisible, + sharedSourcesModalVisible, manageUsersModalVisible, group: { name }, } = useValues(GroupLogic); - const hasMessages = messages.length > 0; - useEffect(() => { initializeGroup(groupId); return resetGroup; @@ -43,7 +40,7 @@ export const GroupRouter: React.FC = () => { return ( <> - {hasMessages && } + @@ -55,7 +52,7 @@ export const GroupRouter: React.FC = () => { - {sharedSourcesModalModalVisible && } + {sharedSourcesModalVisible && } {manageUsersModalVisible && } ); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/groups.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/groups.test.tsx new file mode 100644 index 0000000000000..98f40acb96469 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/groups.test.tsx @@ -0,0 +1,180 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import '../../../__mocks__/kea.mock'; +import '../../../__mocks__/shallow_useeffect.mock'; + +import { setMockActions, setMockValues } from '../../../__mocks__'; +import { groups } from '../../__mocks__/groups.mock'; + +import React from 'react'; +import { shallow } from 'enzyme'; + +import { Groups } from './groups'; + +import { ViewContentHeader } from '../../components/shared/view_content_header'; +import { Loading } from '../../components/shared/loading'; +import { FlashMessages } from '../../../shared/flash_messages'; + +import { AddGroupModal } from './components/add_group_modal'; +import { ClearFiltersLink } from './components/clear_filters_link'; +import { GroupsTable } from './components/groups_table'; +import { TableFilters } from './components/table_filters'; + +import { DEFAULT_META } from '../../../shared/constants'; + +import { EuiFieldSearch, EuiLoadingSpinner } from '@elastic/eui'; +import { EuiButton as EuiLinkButton } from '../../../shared/react_router_helpers'; + +const getSearchResults = jest.fn(); +const openNewGroupModal = jest.fn(); +const resetGroups = jest.fn(); +const setFilterValue = jest.fn(); +const setActivePage = jest.fn(); + +const mockMeta = { + ...DEFAULT_META, + page: { + current: 1, + total_results: 50, + total_pages: 5, + }, +}; + +const mockSuccessMessage = { + type: 'success', + message: 'group added', +}; + +const mockValues = { + groups, + groupsDataLoading: false, + newGroupModalOpen: false, + newGroup: null, + groupListLoading: false, + hasFiltersSet: false, + groupsMeta: mockMeta, + filteredSources: [], + filteredUsers: [], + filterValue: '', + isFederatedAuth: false, +}; + +describe('GroupOverview', () => { + beforeEach(() => { + setMockActions({ + getSearchResults, + openNewGroupModal, + resetGroups, + setFilterValue, + setActivePage, + }); + setMockValues(mockValues); + }); + + it('renders', () => { + const wrapper = shallow(); + + expect(wrapper.find(ViewContentHeader)).toHaveLength(1); + expect(wrapper.find(GroupsTable)).toHaveLength(1); + expect(wrapper.find(TableFilters)).toHaveLength(1); + }); + + it('returns loading when loading', () => { + setMockValues({ ...mockValues, groupsDataLoading: true }); + const wrapper = shallow(); + + expect(wrapper.find(Loading)).toHaveLength(1); + }); + + it('gets search results when filters changed', () => { + const wrapper = shallow(); + + const filters = wrapper.find(TableFilters).dive().shallow(); + const input = filters.find(EuiFieldSearch); + + input.simulate('change', { target: { value: 'Query' } }); + + expect(getSearchResults).toHaveBeenCalledWith(true); + }); + + it('renders manage button when new group added', () => { + setMockValues({ + ...mockValues, + newGroup: { name: 'group', id: '123' }, + messages: [mockSuccessMessage], + }); + const wrapper = shallow(); + const flashMessages = wrapper.find(FlashMessages).dive().shallow(); + + expect(flashMessages.find('[data-test-subj="NewGroupManageButton"]')).toHaveLength(1); + }); + + it('renders ClearFiltersLink when filters set', () => { + setMockValues({ + ...mockValues, + hasFiltersSet: true, + groupsMeta: DEFAULT_META, + }); + + const wrapper = shallow(); + + expect(wrapper.find(ClearFiltersLink)).toHaveLength(1); + }); + + it('renders inviteUsersButton when not federated auth', () => { + setMockValues({ + ...mockValues, + isFederatedAuth: false, + }); + + const wrapper = shallow(); + + const Action: React.FC = () => + wrapper.find(ViewContentHeader).props().action as React.ReactElement | null; + const action = shallow(); + + expect(action.find('[data-test-subj="InviteUsersButton"]')).toHaveLength(1); + expect(action.find(EuiLinkButton)).toHaveLength(1); + }); + + it('does not render inviteUsersButton when federated auth', () => { + setMockValues({ + ...mockValues, + isFederatedAuth: true, + }); + + const wrapper = shallow(); + + const Action: React.FC = () => + wrapper.find(ViewContentHeader).props().action as React.ReactElement | null; + const action = shallow(); + + expect(action.find('[data-test-subj="InviteUsersButton"]')).toHaveLength(0); + }); + + it('renders EuiLoadingSpinner when loading', () => { + setMockValues({ + ...mockValues, + groupListLoading: true, + }); + + const wrapper = shallow(); + + expect(wrapper.find(EuiLoadingSpinner)).toHaveLength(1); + }); + + it('renders AddGroupModal', () => { + setMockValues({ + ...mockValues, + newGroupModalOpen: true, + }); + + const wrapper = shallow(); + + expect(wrapper.find(AddGroupModal)).toHaveLength(1); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/groups.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/groups.tsx index 34a66282a312d..ae87ef735bb9f 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/groups.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/groups.tsx @@ -19,7 +19,6 @@ import { ViewContentHeader } from '../../components/shared/view_content_header'; import { getGroupPath, USERS_PATH } from '../../routes'; -import { useDidUpdateEffect } from '../../../shared/use_did_update_effect'; import { FlashMessages, FlashMessagesLogic } from '../../../shared/flash_messages'; import { GroupsLogic } from './groups_logic'; @@ -40,7 +39,7 @@ export const Groups: React.FC = () => { groupListLoading, hasFiltersSet, groupsMeta: { - page: { current: activePage, total_results: numGroups }, + page: { total_results: numGroups }, }, filteredSources, filteredUsers, @@ -56,18 +55,17 @@ export const Groups: React.FC = () => { return resetGroups; }, [filteredSources, filteredUsers, filterValue]); - // Because the initial search happens above, we want to skip the initial search and use the custom hook to do so. - useDidUpdateEffect(() => { - getSearchResults(); - }, [activePage]); - if (groupsDataLoading) { return ; } if (newGroup && hasMessages) { messages[0].description = ( - + {i18n.translate('xpack.enterpriseSearch.workplaceSearch.groups.newGroup.action', { defaultMessage: 'Manage Group', })} diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/groups_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/groups_logic.test.ts new file mode 100644 index 0000000000000..18206573a3978 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/groups_logic.test.ts @@ -0,0 +1,432 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { resetContext } from 'kea'; + +jest.mock('../../../shared/http', () => ({ + HttpLogic: { + values: { http: { get: jest.fn(), post: jest.fn() } }, + }, +})); +import { HttpLogic } from '../../../shared/http'; + +jest.mock('../../../shared/flash_messages', () => ({ + FlashMessagesLogic: { actions: { clearFlashMessages: jest.fn(), setQueuedMessages: jest.fn() } }, + flashAPIErrors: jest.fn(), + setSuccessMessage: jest.fn(), + setQueuedSuccessMessage: jest.fn(), +})); +import { FlashMessagesLogic, flashAPIErrors } from '../../../shared/flash_messages'; + +import { DEFAULT_META } from '../../../shared/constants'; +import { JSON_HEADER as headers } from '../../../../../common/constants'; + +import { groups } from '../../__mocks__/groups.mock'; +import { contentSources } from '../../__mocks__/content_sources.mock'; +import { users } from '../../__mocks__/users.mock'; +import { mockGroupsValues } from './__mocks__/groups_logic.mock'; +import { GroupsLogic } from './groups_logic'; + +// We need to mock out the debounced functionality +const TIMEOUT = 400; +const delay = () => new Promise((resolve) => setTimeout(resolve, TIMEOUT)); + +describe('GroupsLogic', () => { + const clearFlashMessagesSpy = jest.spyOn(FlashMessagesLogic.actions, 'clearFlashMessages'); + const groupsResponse = { + results: groups, + meta: DEFAULT_META, + }; + + beforeEach(() => { + jest.clearAllMocks(); + resetContext({}); + GroupsLogic.mount(); + }); + + it('has expected default values', () => { + expect(GroupsLogic.values).toEqual(mockGroupsValues); + }); + + describe('actions', () => { + describe('onInitializeGroups', () => { + it('sets reducers', () => { + GroupsLogic.actions.onInitializeGroups({ contentSources, users }); + + expect(GroupsLogic.values.groupsDataLoading).toEqual(false); + expect(GroupsLogic.values.contentSources).toEqual(contentSources); + expect(GroupsLogic.values.users).toEqual(users); + }); + }); + + describe('setSearchResults', () => { + it('sets reducers', () => { + GroupsLogic.actions.setSearchResults(groupsResponse); + + expect(GroupsLogic.values.groups).toEqual(groups); + expect(GroupsLogic.values.groupListLoading).toEqual(false); + expect(GroupsLogic.values.newGroupName).toEqual(''); + expect(GroupsLogic.values.groupsMeta).toEqual(DEFAULT_META); + }); + }); + + describe('addFilteredSource', () => { + it('sets reducers', () => { + GroupsLogic.actions.addFilteredSource('foo'); + GroupsLogic.actions.addFilteredSource('bar'); + GroupsLogic.actions.addFilteredSource('baz'); + + expect(GroupsLogic.values.filteredSources).toEqual(['bar', 'baz', 'foo']); + }); + }); + + describe('removeFilteredSource', () => { + it('sets reducers', () => { + GroupsLogic.actions.addFilteredSource('foo'); + GroupsLogic.actions.addFilteredSource('bar'); + GroupsLogic.actions.addFilteredSource('baz'); + GroupsLogic.actions.removeFilteredSource('foo'); + + expect(GroupsLogic.values.filteredSources).toEqual(['bar', 'baz']); + }); + }); + + describe('addFilteredUser', () => { + it('sets reducers', () => { + GroupsLogic.actions.addFilteredUser('foo'); + GroupsLogic.actions.addFilteredUser('bar'); + GroupsLogic.actions.addFilteredUser('baz'); + + expect(GroupsLogic.values.filteredUsers).toEqual(['bar', 'baz', 'foo']); + }); + }); + + describe('removeFilteredUser', () => { + it('sets reducers', () => { + GroupsLogic.actions.addFilteredUser('foo'); + GroupsLogic.actions.addFilteredUser('bar'); + GroupsLogic.actions.addFilteredUser('baz'); + GroupsLogic.actions.removeFilteredUser('foo'); + + expect(GroupsLogic.values.filteredUsers).toEqual(['bar', 'baz']); + }); + }); + + describe('setGroupUsers', () => { + it('sets reducers', () => { + GroupsLogic.actions.setGroupUsers(users); + + expect(GroupsLogic.values.allGroupUsersLoading).toEqual(false); + expect(GroupsLogic.values.allGroupUsers).toEqual(users); + }); + }); + + describe('setAllGroupLoading', () => { + it('sets reducer', () => { + GroupsLogic.actions.setAllGroupLoading(true); + + expect(GroupsLogic.values.allGroupUsersLoading).toEqual(true); + expect(GroupsLogic.values.allGroupUsers).toEqual([]); + }); + }); + + describe('setFilterValue', () => { + it('sets reducer', () => { + GroupsLogic.actions.setFilterValue('foo'); + + expect(GroupsLogic.values.filterValue).toEqual('foo'); + }); + }); + + describe('setNewGroupName', () => { + it('sets reducer', () => { + const NEW_NAME = 'new name'; + GroupsLogic.actions.setNewGroupName(NEW_NAME); + + expect(GroupsLogic.values.newGroupName).toEqual(NEW_NAME); + expect(GroupsLogic.values.newGroupNameErrors).toEqual([]); + }); + }); + + describe('setNewGroup', () => { + it('sets reducer', () => { + GroupsLogic.actions.setNewGroup(groups[0]); + + expect(GroupsLogic.values.newGroupModalOpen).toEqual(false); + expect(GroupsLogic.values.newGroup).toEqual(groups[0]); + expect(GroupsLogic.values.newGroupNameErrors).toEqual([]); + expect(GroupsLogic.values.filteredSources).toEqual([]); + expect(GroupsLogic.values.filteredUsers).toEqual([]); + expect(GroupsLogic.values.groupsMeta).toEqual(DEFAULT_META); + }); + }); + + describe('setNewGroupFormErrors', () => { + it('sets reducer', () => { + const errors = ['this is an error']; + GroupsLogic.actions.setNewGroupFormErrors(errors); + + expect(GroupsLogic.values.newGroupNameErrors).toEqual(errors); + }); + }); + + describe('closeNewGroupModal', () => { + it('sets reducer', () => { + GroupsLogic.actions.closeNewGroupModal(); + + expect(GroupsLogic.values.newGroupModalOpen).toEqual(false); + expect(GroupsLogic.values.newGroupName).toEqual(''); + expect(GroupsLogic.values.newGroupNameErrors).toEqual([]); + }); + }); + + describe('closeFilterSourcesDropdown', () => { + it('sets reducer', () => { + // Open dropdown first + GroupsLogic.actions.toggleFilterSourcesDropdown(); + GroupsLogic.actions.closeFilterSourcesDropdown(); + + expect(GroupsLogic.values.filterSourcesDropdownOpen).toEqual(false); + }); + }); + + describe('closeFilterUsersDropdown', () => { + it('sets reducer', () => { + // Open dropdown first + GroupsLogic.actions.toggleFilterUsersDropdown(); + GroupsLogic.actions.closeFilterUsersDropdown(); + + expect(GroupsLogic.values.filterUsersDropdownOpen).toEqual(false); + }); + }); + + describe('setGroupsLoading', () => { + it('sets reducer', () => { + // Set to false first + GroupsLogic.actions.setSearchResults(groupsResponse); + GroupsLogic.actions.setGroupsLoading(); + + expect(GroupsLogic.values.groupListLoading).toEqual(true); + }); + }); + + describe('resetGroups', () => { + it('sets reducer', () => { + GroupsLogic.actions.resetGroups(); + + expect(GroupsLogic.values.newGroup).toEqual(null); + }); + }); + }); + + describe('listeners', () => { + describe('initializeGroups', () => { + it('calls API and sets values', async () => { + const onInitializeGroupsSpy = jest.spyOn(GroupsLogic.actions, 'onInitializeGroups'); + const promise = Promise.resolve(groupsResponse); + (HttpLogic.values.http.get as jest.Mock).mockReturnValue(promise); + + GroupsLogic.actions.initializeGroups(); + expect(HttpLogic.values.http.get).toHaveBeenCalledWith('/api/workplace_search/groups'); + await promise; + expect(onInitializeGroupsSpy).toHaveBeenCalledWith(groupsResponse); + }); + + it('handles error', async () => { + const promise = Promise.reject('this is an error'); + (HttpLogic.values.http.get as jest.Mock).mockReturnValue(promise); + + GroupsLogic.actions.initializeGroups(); + try { + await promise; + } catch { + expect(flashAPIErrors).toHaveBeenCalledWith('this is an error'); + } + }); + }); + + describe('getSearchResults', () => { + const search = { + query: '', + content_source_ids: [], + user_ids: [], + }; + + const payload = { + body: JSON.stringify({ + page: { + current: 1, + size: 10, + }, + search, + }), + headers, + }; + + it('calls API and sets values', async () => { + const setSearchResultsSpy = jest.spyOn(GroupsLogic.actions, 'setSearchResults'); + const promise = Promise.resolve(groups); + (HttpLogic.values.http.post as jest.Mock).mockReturnValue(promise); + + GroupsLogic.actions.getSearchResults(); + await delay(); + expect(HttpLogic.values.http.post).toHaveBeenCalledWith( + '/api/workplace_search/groups/search', + payload + ); + await promise; + expect(setSearchResultsSpy).toHaveBeenCalledWith(groups); + }); + + it('calls API and resets pagination', async () => { + // Set active page to 2 to confirm resetting sends the `payload` value of 1 for the current page. + GroupsLogic.actions.setActivePage(2); + const setSearchResultsSpy = jest.spyOn(GroupsLogic.actions, 'setSearchResults'); + const promise = Promise.resolve(groups); + (HttpLogic.values.http.post as jest.Mock).mockReturnValue(promise); + + GroupsLogic.actions.getSearchResults(true); + // Account for `breakpoint` that debounces filter value. + await delay(); + expect(HttpLogic.values.http.post).toHaveBeenCalledWith( + '/api/workplace_search/groups/search', + payload + ); + await promise; + expect(setSearchResultsSpy).toHaveBeenCalledWith(groups); + }); + + it('handles error', async () => { + const promise = Promise.reject('this is an error'); + (HttpLogic.values.http.post as jest.Mock).mockReturnValue(promise); + + GroupsLogic.actions.getSearchResults(); + try { + await promise; + } catch { + // Account for `breakpoint` that debounces filter value. + await delay(); + expect(flashAPIErrors).toHaveBeenCalledWith('this is an error'); + } + }); + }); + + describe('fetchGroupUsers', () => { + it('calls API and sets values', async () => { + const setGroupUsersSpy = jest.spyOn(GroupsLogic.actions, 'setGroupUsers'); + const promise = Promise.resolve(users); + (HttpLogic.values.http.get as jest.Mock).mockReturnValue(promise); + + GroupsLogic.actions.fetchGroupUsers('123'); + expect(HttpLogic.values.http.get).toHaveBeenCalledWith( + '/api/workplace_search/groups/123/group_users' + ); + await promise; + expect(setGroupUsersSpy).toHaveBeenCalledWith(users); + }); + + it('handles error', async () => { + const promise = Promise.reject('this is an error'); + (HttpLogic.values.http.get as jest.Mock).mockReturnValue(promise); + + GroupsLogic.actions.fetchGroupUsers('123'); + try { + await promise; + } catch { + expect(flashAPIErrors).toHaveBeenCalledWith('this is an error'); + } + }); + }); + + describe('saveNewGroup', () => { + it('calls API and sets values', async () => { + const GROUP_NAME = 'new group'; + GroupsLogic.actions.setNewGroupName(GROUP_NAME); + const setNewGroupSpy = jest.spyOn(GroupsLogic.actions, 'setNewGroup'); + const promise = Promise.resolve(groups[0]); + (HttpLogic.values.http.post as jest.Mock).mockReturnValue(promise); + + GroupsLogic.actions.saveNewGroup(); + expect(HttpLogic.values.http.post).toHaveBeenCalledWith('/api/workplace_search/groups', { + body: JSON.stringify({ group_name: GROUP_NAME }), + headers, + }); + await promise; + expect(setNewGroupSpy).toHaveBeenCalledWith(groups[0]); + }); + + it('handles error', async () => { + const promise = Promise.reject('this is an error'); + (HttpLogic.values.http.post as jest.Mock).mockReturnValue(promise); + + GroupsLogic.actions.saveNewGroup(); + try { + await promise; + } catch { + expect(flashAPIErrors).toHaveBeenCalledWith('this is an error'); + } + }); + }); + + describe('setActivePage', () => { + it('sets reducer', () => { + const getSearchResultsSpy = jest.spyOn(GroupsLogic.actions, 'getSearchResults'); + const activePage = 3; + GroupsLogic.actions.setActivePage(activePage); + + expect(GroupsLogic.values.groupsMeta).toEqual({ + ...DEFAULT_META, + page: { + ...DEFAULT_META.page, + current: activePage, + }, + }); + + expect(getSearchResultsSpy).toHaveBeenCalled(); + }); + }); + + describe('openNewGroupModal', () => { + it('sets reducer and clears flash messages', () => { + GroupsLogic.actions.openNewGroupModal(); + + expect(GroupsLogic.values.newGroupModalOpen).toEqual(true); + expect(GroupsLogic.values.newGroup).toEqual(null); + expect(clearFlashMessagesSpy).toHaveBeenCalled(); + }); + }); + + describe('resetGroupsFilters', () => { + it('sets reducer and clears flash messages', () => { + GroupsLogic.actions.resetGroupsFilters(); + + expect(GroupsLogic.values.filteredSources).toEqual([]); + expect(GroupsLogic.values.filteredUsers).toEqual([]); + expect(GroupsLogic.values.filterValue).toEqual(''); + expect(GroupsLogic.values.groupsMeta).toEqual(DEFAULT_META); + expect(clearFlashMessagesSpy).toHaveBeenCalled(); + }); + }); + + describe('toggleFilterSourcesDropdown', () => { + it('sets reducer and clears flash messages', () => { + GroupsLogic.actions.toggleFilterSourcesDropdown(); + + expect(GroupsLogic.values.filterSourcesDropdownOpen).toEqual(true); + expect(clearFlashMessagesSpy).toHaveBeenCalled(); + }); + }); + + describe('toggleFilterUsersDropdown', () => { + it('sets reducer and clears flash messages', () => { + GroupsLogic.actions.toggleFilterUsersDropdown(); + + expect(GroupsLogic.values.filterUsersDropdownOpen).toEqual(true); + expect(clearFlashMessagesSpy).toHaveBeenCalled(); + }); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/groups_logic.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/groups_logic.ts index 35d4387b4cf3d..685a2651cb24a 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/groups_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/groups_logic.ts @@ -335,6 +335,9 @@ export const GroupsLogic = kea>({ flashAPIErrors(e); } }, + setActivePage: () => { + actions.getSearchResults(); + }, openNewGroupModal: () => { FlashMessagesLogic.actions.clearFlashMessages(); }, diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/groups_router.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/groups_router.test.tsx new file mode 100644 index 0000000000000..0b2b1ad05dfd7 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/groups_router.test.tsx @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import '../../../__mocks__/kea.mock'; +import '../../../__mocks__/shallow_useeffect.mock'; + +import { setMockActions } from '../../../__mocks__'; + +import React from 'react'; +import { shallow } from 'enzyme'; + +import { Route, Switch } from 'react-router-dom'; + +import { GroupsRouter } from './groups_router'; + +import { GroupRouter } from './group_router'; +import { Groups } from './groups'; + +describe('GroupsRouter', () => { + const initializeGroups = jest.fn(); + + beforeEach(() => { + setMockActions({ initializeGroups }); + }); + + it('renders', () => { + const wrapper = shallow(); + + expect(wrapper.find(Switch)).toHaveLength(1); + expect(wrapper.find(Route)).toHaveLength(2); + expect(wrapper.find(GroupRouter)).toHaveLength(1); + expect(wrapper.find(Groups)).toHaveLength(1); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/groups_router.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/groups_router.tsx index adfa10d37c524..a4fe472065d90 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/groups_router.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/groups_router.tsx @@ -37,7 +37,9 @@ export const GroupsRouter: React.FC = () => { - + + + ); };