diff options
Diffstat (limited to 'spec/frontend/admin')
11 files changed, 336 insertions, 35 deletions
diff --git a/spec/frontend/admin/signup_restrictions/components/signup_form_spec.js b/spec/frontend/admin/signup_restrictions/components/signup_form_spec.js index 411126d0c89..e6718f62b91 100644 --- a/spec/frontend/admin/signup_restrictions/components/signup_form_spec.js +++ b/spec/frontend/admin/signup_restrictions/components/signup_form_spec.js @@ -1,4 +1,4 @@ -import { GlButton, GlModal } from '@gitlab/ui'; +import { GlButton, GlModal, GlLink } from '@gitlab/ui'; import { within } from '@testing-library/dom'; import { shallowMount, mount, createWrapper } from '@vue/test-utils'; import { stubComponent } from 'helpers/stub_component'; @@ -36,6 +36,7 @@ describe('Signup Form', () => { const findDenyListRawInputGroup = () => wrapper.findByTestId('domain-denylist-raw-input-group'); const findDenyListFileInputGroup = () => wrapper.findByTestId('domain-denylist-file-input-group'); const findUserCapInput = () => wrapper.findByTestId('user-cap-input'); + const findUserCapFormGroup = () => wrapper.findByTestId('user-cap-form-group'); const findModal = () => wrapper.findComponent(GlModal); afterEach(() => { @@ -214,4 +215,19 @@ describe('Signup Form', () => { }); }); }); + + describe('rendering help links within user cap description', () => { + beforeEach(() => { + mountComponent({ mountFn: mount }); + }); + + it('renders projectSharingHelpLink and groupSharingHelpLink', () => { + const [projectSharingLink, groupSharingLink] = findUserCapFormGroup().findAllComponents( + GlLink, + ).wrappers; + + expect(projectSharingLink.attributes('href')).toBe(mockData.projectSharingHelpLink); + expect(groupSharingLink.attributes('href')).toBe(mockData.groupSharingHelpLink); + }); + }); }); diff --git a/spec/frontend/admin/signup_restrictions/mock_data.js b/spec/frontend/admin/signup_restrictions/mock_data.js index 9e001e122a4..dd1ed317497 100644 --- a/spec/frontend/admin/signup_restrictions/mock_data.js +++ b/spec/frontend/admin/signup_restrictions/mock_data.js @@ -4,6 +4,7 @@ export const rawMockData = { signupEnabled: 'true', requireAdminApprovalAfterUserSignup: 'true', sendUserConfirmationEmail: 'true', + emailConfirmationSetting: 'hard', minimumPasswordLength: '8', minimumPasswordLengthMin: '3', minimumPasswordLengthMax: '10', @@ -22,6 +23,8 @@ export const rawMockData = { passwordLowercaseRequired: 'true', passwordUppercaseRequired: 'true', passwordSymbolRequired: 'true', + projectSharingHelpLink: 'project-sharing/help/link', + groupSharingHelpLink: 'group-sharing/help/link', }; export const mockData = { @@ -30,6 +33,7 @@ export const mockData = { signupEnabled: true, requireAdminApprovalAfterUserSignup: true, sendUserConfirmationEmail: true, + emailConfirmationSetting: 'hard', minimumPasswordLength: '8', minimumPasswordLengthMin: '3', minimumPasswordLengthMax: '10', @@ -48,4 +52,6 @@ export const mockData = { passwordLowercaseRequired: true, passwordUppercaseRequired: true, passwordSymbolRequired: true, + projectSharingHelpLink: 'project-sharing/help/link', + groupSharingHelpLink: 'group-sharing/help/link', }; diff --git a/spec/frontend/admin/users/components/actions/actions_spec.js b/spec/frontend/admin/users/components/actions/actions_spec.js index 4967753b91c..8e9652332c1 100644 --- a/spec/frontend/admin/users/components/actions/actions_spec.js +++ b/spec/frontend/admin/users/components/actions/actions_spec.js @@ -1,13 +1,13 @@ import { GlDropdownItem } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; import Actions from '~/admin/users/components/actions'; +import Delete from '~/admin/users/components/actions/delete.vue'; import eventHub, { EVENT_OPEN_DELETE_USER_MODAL, } from '~/admin/users/components/modals/delete_user_modal_event_hub'; import { capitalizeFirstCharacter } from '~/lib/utils/text_utility'; -import { OBSTACLE_TYPES } from '~/vue_shared/components/user_deletion_obstacles/constants'; -import { CONFIRMATION_ACTIONS, DELETE_ACTIONS } from '../../constants'; -import { paths } from '../../mock_data'; +import { CONFIRMATION_ACTIONS } from '../../constants'; +import { paths, userDeletionObstacles } from '../../mock_data'; describe('Action components', () => { let wrapper; @@ -41,40 +41,33 @@ describe('Action components', () => { }); }); - describe('DELETE_ACTION_COMPONENTS', () => { + describe('DELETE', () => { beforeEach(() => { jest.spyOn(eventHub, '$emit').mockImplementation(); }); - const userDeletionObstacles = [ - { name: 'schedule1', type: OBSTACLE_TYPES.oncallSchedules }, - { name: 'policy1', type: OBSTACLE_TYPES.escalationPolicies }, - ]; - - it.each(DELETE_ACTIONS)( - 'renders a dropdown item that opens the delete user modal when clicked for "%s"', - async (action) => { - initComponent({ - component: Actions[capitalizeFirstCharacter(action)], - props: { - username: 'John Doe', - paths, - userDeletionObstacles, - }, - }); + it('renders a dropdown item that opens the delete user modal when Delete is clicked', async () => { + initComponent({ + component: Delete, + props: { + username: 'John Doe', + userId: 1, + paths, + userDeletionObstacles, + }, + }); - await findDropdownItem().vm.$emit('click'); + await findDropdownItem().vm.$emit('click'); - expect(eventHub.$emit).toHaveBeenCalledWith( - EVENT_OPEN_DELETE_USER_MODAL, - expect.objectContaining({ - username: 'John Doe', - blockPath: paths.block, - deletePath: paths[action], - userDeletionObstacles, - }), - ); - }, - ); + expect(eventHub.$emit).toHaveBeenCalledWith( + EVENT_OPEN_DELETE_USER_MODAL, + expect.objectContaining({ + username: 'John Doe', + blockPath: paths.block, + deletePath: paths.delete, + userDeletionObstacles, + }), + ); + }); }); }); diff --git a/spec/frontend/admin/users/components/actions/delete_with_contributions_spec.js b/spec/frontend/admin/users/components/actions/delete_with_contributions_spec.js new file mode 100644 index 00000000000..64a88aab2c2 --- /dev/null +++ b/spec/frontend/admin/users/components/actions/delete_with_contributions_spec.js @@ -0,0 +1,107 @@ +import { GlLoadingIcon } from '@gitlab/ui'; +import { mountExtended } from 'helpers/vue_test_utils_helper'; +import waitForPromises from 'helpers/wait_for_promises'; +import DeleteWithContributions from '~/admin/users/components/actions/delete_with_contributions.vue'; +import eventHub, { + EVENT_OPEN_DELETE_USER_MODAL, +} from '~/admin/users/components/modals/delete_user_modal_event_hub'; +import { associationsCount } from '~/api/user_api'; +import { + paths, + associationsCount as associationsCountData, + userDeletionObstacles, +} from '../../mock_data'; + +jest.mock('~/admin/users/components/modals/delete_user_modal_event_hub', () => ({ + ...jest.requireActual('~/admin/users/components/modals/delete_user_modal_event_hub'), + __esModule: true, + default: { + $emit: jest.fn(), + }, +})); + +jest.mock('~/api/user_api', () => ({ + associationsCount: jest.fn(), +})); + +describe('DeleteWithContributions', () => { + let wrapper; + + const defaultPropsData = { + username: 'John Doe', + userId: 1, + paths, + userDeletionObstacles, + }; + + const createComponent = () => { + wrapper = mountExtended(DeleteWithContributions, { propsData: defaultPropsData }); + }; + + describe('when action is clicked', () => { + describe('when API request is loading', () => { + beforeEach(() => { + associationsCount.mockReturnValueOnce(new Promise(() => {})); + + createComponent(); + }); + + it('displays loading icon and disables button', async () => { + await wrapper.trigger('click'); + + expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true); + expect(wrapper.findByRole('menuitem').attributes()).toMatchObject({ + disabled: 'disabled', + 'aria-busy': 'true', + }); + }); + }); + + describe('when API request is successful', () => { + beforeEach(() => { + associationsCount.mockResolvedValueOnce({ + data: associationsCountData, + }); + + createComponent(); + }); + + it('emits event with association counts', async () => { + await wrapper.trigger('click'); + await waitForPromises(); + + expect(associationsCount).toHaveBeenCalledWith(defaultPropsData.userId); + expect(eventHub.$emit).toHaveBeenCalledWith( + EVENT_OPEN_DELETE_USER_MODAL, + expect.objectContaining({ + associationsCount: associationsCountData, + username: defaultPropsData.username, + blockPath: paths.block, + deletePath: paths.deleteWithContributions, + userDeletionObstacles, + }), + ); + }); + }); + + describe('when API request is not successful', () => { + beforeEach(() => { + associationsCount.mockRejectedValueOnce(); + + createComponent(); + }); + + it('emits event with error', async () => { + await wrapper.trigger('click'); + await waitForPromises(); + + expect(eventHub.$emit).toHaveBeenCalledWith( + EVENT_OPEN_DELETE_USER_MODAL, + expect.objectContaining({ + associationsCount: new Error(), + }), + ); + }); + }); + }); +}); diff --git a/spec/frontend/admin/users/components/associations/__snapshots__/associations_list_item_spec.js.snap b/spec/frontend/admin/users/components/associations/__snapshots__/associations_list_item_spec.js.snap new file mode 100644 index 00000000000..4237685e45c --- /dev/null +++ b/spec/frontend/admin/users/components/associations/__snapshots__/associations_list_item_spec.js.snap @@ -0,0 +1,3 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`AssociationsListItem renders interpolated message in a \`li\` element 1`] = `"<li><strong>5</strong> groups</li>"`; diff --git a/spec/frontend/admin/users/components/associations/__snapshots__/associations_list_spec.js.snap b/spec/frontend/admin/users/components/associations/__snapshots__/associations_list_spec.js.snap new file mode 100644 index 00000000000..dc98d367af7 --- /dev/null +++ b/spec/frontend/admin/users/components/associations/__snapshots__/associations_list_spec.js.snap @@ -0,0 +1,34 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`AssociationsList when counts are 0 does not render items 1`] = `""`; + +exports[`AssociationsList when counts are plural renders plural counts 1`] = ` +"<ul class=\\"gl-mb-5\\"> + <li><strong>2</strong> groups</li> + <li><strong>3</strong> projects</li> + <li><strong>4</strong> issues</li> + <li><strong>5</strong> merge requests</li> +</ul>" +`; + +exports[`AssociationsList when counts are singular renders singular counts 1`] = ` +"<ul class=\\"gl-mb-5\\"> + <li><strong>1</strong> group</li> + <li><strong>1</strong> project</li> + <li><strong>1</strong> issue</li> + <li><strong>1</strong> merge request</li> +</ul>" +`; + +exports[`AssociationsList when there is an error displays an alert 1`] = ` +"<div class=\\"gl-mb-5 gl-alert gl-alert-not-dismissible gl-alert-danger\\"><svg data-testid=\\"error-icon\\" role=\\"img\\" aria-hidden=\\"true\\" class=\\"gl-icon s16 gl-alert-icon gl-alert-icon-no-title\\"> + <use href=\\"#error\\"></use> + </svg> + <div role=\\"alert\\" aria-live=\\"assertive\\" class=\\"gl-alert-content\\"> + <!----> + <div class=\\"gl-alert-body\\">An error occurred while fetching this user's contributions, and the request cannot return the number of issues, merge requests, groups, and projects linked to this user. If you proceed with deleting the user, all their contributions will still be deleted.</div> + <!----> + </div> + <!----> +</div>" +`; diff --git a/spec/frontend/admin/users/components/associations/associations_list_item_spec.js b/spec/frontend/admin/users/components/associations/associations_list_item_spec.js new file mode 100644 index 00000000000..5126df12c24 --- /dev/null +++ b/spec/frontend/admin/users/components/associations/associations_list_item_spec.js @@ -0,0 +1,25 @@ +import { mountExtended } from 'helpers/vue_test_utils_helper'; +import AssociationsListItem from '~/admin/users/components/associations/associations_list_item.vue'; +import { n__ } from '~/locale'; + +describe('AssociationsListItem', () => { + let wrapper; + const count = 5; + + const createComponent = () => { + wrapper = mountExtended(AssociationsListItem, { + propsData: { + message: n__('%{count} group', '%{count} groups', count), + count, + }, + }); + }; + + beforeEach(() => { + createComponent(); + }); + + it('renders interpolated message in a `li` element', () => { + expect(wrapper.html()).toMatchSnapshot(); + }); +}); diff --git a/spec/frontend/admin/users/components/associations/associations_list_spec.js b/spec/frontend/admin/users/components/associations/associations_list_spec.js new file mode 100644 index 00000000000..d77a645111f --- /dev/null +++ b/spec/frontend/admin/users/components/associations/associations_list_spec.js @@ -0,0 +1,78 @@ +import { mountExtended } from 'helpers/vue_test_utils_helper'; +import AssociationsList from '~/admin/users/components/associations/associations_list.vue'; + +describe('AssociationsList', () => { + let wrapper; + + const defaultPropsData = { + associationsCount: { + groups_count: 1, + projects_count: 1, + issues_count: 1, + merge_requests_count: 1, + }, + }; + + const createComponent = ({ propsData = {} } = {}) => { + wrapper = mountExtended(AssociationsList, { + propsData: { + ...defaultPropsData, + ...propsData, + }, + }); + }; + + describe('when there is an error', () => { + it('displays an alert', () => { + createComponent({ + propsData: { + associationsCount: new Error(), + }, + }); + + expect(wrapper.html()).toMatchSnapshot(); + }); + }); + + describe('when counts are singular', () => { + it('renders singular counts', () => { + createComponent(); + + expect(wrapper.html()).toMatchSnapshot(); + }); + }); + + describe('when counts are plural', () => { + it('renders plural counts', () => { + createComponent({ + propsData: { + associationsCount: { + groups_count: 2, + projects_count: 3, + issues_count: 4, + merge_requests_count: 5, + }, + }, + }); + + expect(wrapper.html()).toMatchSnapshot(); + }); + }); + + describe('when counts are 0', () => { + it('does not render items', () => { + createComponent({ + propsData: { + associationsCount: { + groups_count: 0, + projects_count: 0, + issues_count: 0, + merge_requests_count: 0, + }, + }, + }); + + expect(wrapper.html()).toMatchSnapshot(); + }); + }); +}); diff --git a/spec/frontend/admin/users/components/modals/delete_user_modal_spec.js b/spec/frontend/admin/users/components/modals/delete_user_modal_spec.js index 70ed9eeb3e1..2e892e292d7 100644 --- a/spec/frontend/admin/users/components/modals/delete_user_modal_spec.js +++ b/spec/frontend/admin/users/components/modals/delete_user_modal_spec.js @@ -1,10 +1,12 @@ import { GlButton, GlFormInput, GlSprintf } from '@gitlab/ui'; +import { nextTick } from 'vue'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import eventHub, { EVENT_OPEN_DELETE_USER_MODAL, } from '~/admin/users/components/modals/delete_user_modal_event_hub'; import DeleteUserModal from '~/admin/users/components/modals/delete_user_modal.vue'; import UserDeletionObstaclesList from '~/vue_shared/components/user_deletion_obstacles/user_deletion_obstacles_list.vue'; +import AssociationsList from '~/admin/users/components/associations/associations_list.vue'; import ModalStub from './stubs/modal_stub'; const TEST_DELETE_USER_URL = 'delete-url'; @@ -200,4 +202,24 @@ describe('Delete user modal', () => { expect(obstacles.props('obstacles')).toEqual(userDeletionObstacles); }); }); + + it('renders `AssociationsList` component and passes `associationsCount` prop', async () => { + const associationsCount = { + groups_count: 5, + projects_count: 0, + issues_count: 5, + merge_requests_count: 5, + }; + + createComponent(); + emitOpenModalEvent({ + ...mockModalData, + associationsCount, + }); + await nextTick(); + + expect(wrapper.findComponent(AssociationsList).props('associationsCount')).toEqual( + associationsCount, + ); + }); }); diff --git a/spec/frontend/admin/users/components/user_actions_spec.js b/spec/frontend/admin/users/components/user_actions_spec.js index ffc05e744c8..1b080b05c95 100644 --- a/spec/frontend/admin/users/components/user_actions_spec.js +++ b/spec/frontend/admin/users/components/user_actions_spec.js @@ -121,8 +121,11 @@ describe('AdminUserActions component', () => { it.each(DELETE_ACTIONS)('renders a delete action component item for "%s"', (action) => { const component = wrapper.findComponent(Actions[capitalizeFirstCharacter(action)]); - expect(component.props('username')).toBe(user.name); - expect(component.props('paths')).toEqual(userPaths); + expect(component.props()).toMatchObject({ + username: user.name, + userId: user.id, + paths: userPaths, + }); expect(component.text()).toBe(I18N_USER_ACTIONS[action]); }); }); diff --git a/spec/frontend/admin/users/mock_data.js b/spec/frontend/admin/users/mock_data.js index 73fa73c0b47..193ac3fa043 100644 --- a/spec/frontend/admin/users/mock_data.js +++ b/spec/frontend/admin/users/mock_data.js @@ -1,3 +1,5 @@ +import { OBSTACLE_TYPES } from '~/vue_shared/components/user_deletion_obstacles/constants'; + export const users = [ { id: 2177, @@ -48,3 +50,15 @@ export const createGroupCountResponse = (groupCounts) => ({ }, }, }); + +export const associationsCount = { + groups_count: 5, + projects_count: 5, + issues_count: 5, + merge_requests_count: 5, +}; + +export const userDeletionObstacles = [ + { name: 'schedule1', type: OBSTACLE_TYPES.oncallSchedules }, + { name: 'policy1', type: OBSTACLE_TYPES.escalationPolicies }, +]; |