diff options
Diffstat (limited to 'spec/frontend/invite_members/components')
6 files changed, 137 insertions, 111 deletions
diff --git a/spec/frontend/invite_members/components/group_select_spec.js b/spec/frontend/invite_members/components/group_select_spec.js index 192f3fdd381..e1563a7bb3a 100644 --- a/spec/frontend/invite_members/components/group_select_spec.js +++ b/spec/frontend/invite_members/components/group_select_spec.js @@ -4,7 +4,6 @@ import waitForPromises from 'helpers/wait_for_promises'; import * as groupsApi from '~/api/groups_api'; import GroupSelect from '~/invite_members/components/group_select.vue'; -const accessLevels = { Guest: 10, Reporter: 20, Developer: 30, Maintainer: 40, Owner: 50 }; const group1 = { id: 1, full_name: 'Group One', avatar_url: 'test' }; const group2 = { id: 2, full_name: 'Group Two', avatar_url: 'test' }; const allGroups = [group1, group2]; @@ -13,7 +12,6 @@ const createComponent = (props = {}) => { return mount(GroupSelect, { propsData: { invalidGroups: [], - accessLevels, ...props, }, }); @@ -66,9 +64,8 @@ describe('GroupSelect', () => { resolveApiRequest({ data: allGroups }); expect(groupsApi.getGroups).toHaveBeenCalledWith(group1.name, { - active: true, exclude_internal: true, - min_access_level: accessLevels.Guest, + active: true, }); }); diff --git a/spec/frontend/invite_members/components/invite_groups_modal_spec.js b/spec/frontend/invite_members/components/invite_groups_modal_spec.js index 8085f48f6e2..f9cb4a149f2 100644 --- a/spec/frontend/invite_members/components/invite_groups_modal_spec.js +++ b/spec/frontend/invite_members/components/invite_groups_modal_spec.js @@ -42,18 +42,19 @@ describe('InviteGroupsModal', () => { wrapper = null; }); + const findModal = () => wrapper.findComponent(GlModal); const findGroupSelect = () => wrapper.findComponent(GroupSelect); const findIntroText = () => wrapper.findByTestId('modal-base-intro-text').text(); - const findCancelButton = () => wrapper.findByTestId('cancel-button'); - const findInviteButton = () => wrapper.findByTestId('invite-button'); const findMembersFormGroup = () => wrapper.findByTestId('members-form-group'); const membersFormGroupInvalidFeedback = () => findMembersFormGroup().attributes('invalid-feedback'); - const clickInviteButton = () => findInviteButton().vm.$emit('click'); - const clickCancelButton = () => findCancelButton().vm.$emit('click'); - const triggerGroupSelect = (val) => findGroupSelect().vm.$emit('input', val); const findBase = () => wrapper.findComponent(InviteModalBase); - const hideModal = () => wrapper.findComponent(GlModal).vm.$emit('hide'); + const triggerGroupSelect = (val) => findGroupSelect().vm.$emit('input', val); + const emitEventFromModal = (eventName) => () => + findModal().vm.$emit(eventName, { preventDefault: jest.fn() }); + const hideModal = emitEventFromModal('hidden'); + const clickInviteButton = emitEventFromModal('primary'); + const clickCancelButton = emitEventFromModal('cancel'); describe('displaying the correct introText and form group description', () => { describe('when inviting to a project', () => { diff --git a/spec/frontend/invite_members/components/invite_members_modal_spec.js b/spec/frontend/invite_members/components/invite_members_modal_spec.js index dd16bb48cb8..84317da39e6 100644 --- a/spec/frontend/invite_members/components/invite_members_modal_spec.js +++ b/spec/frontend/invite_members/components/invite_members_modal_spec.js @@ -23,7 +23,7 @@ import ContentTransition from '~/vue_shared/components/content_transition.vue'; import axios from '~/lib/utils/axios_utils'; import httpStatus from '~/lib/utils/http_status'; import { getParameterValues } from '~/lib/utils/url_utility'; -import { apiPaths, membersApiResponse, invitationsApiResponse } from '../mock_data/api_responses'; +import { GROUPS_INVITATIONS_PATH, invitationsApiResponse } from '../mock_data/api_responses'; import { propsData, inviteSource, @@ -85,12 +85,13 @@ describe('InviteMembersModal', () => { mock.restore(); }); + const findModal = () => wrapper.findComponent(GlModal); const findBase = () => wrapper.findComponent(InviteModalBase); const findIntroText = () => wrapper.findByTestId('modal-base-intro-text').text(); - const findCancelButton = () => wrapper.findByTestId('cancel-button'); - const findInviteButton = () => wrapper.findByTestId('invite-button'); - const clickInviteButton = () => findInviteButton().vm.$emit('click'); - const clickCancelButton = () => findCancelButton().vm.$emit('click'); + const emitEventFromModal = (eventName) => () => + findModal().vm.$emit(eventName, { preventDefault: jest.fn() }); + const clickInviteButton = emitEventFromModal('primary'); + const clickCancelButton = emitEventFromModal('cancel'); const findMembersFormGroup = () => wrapper.findByTestId('members-form-group'); const membersFormGroupInvalidFeedback = () => findMembersFormGroup().attributes('invalid-feedback'); @@ -276,7 +277,7 @@ describe('InviteMembersModal', () => { }); it('renders the modal with the correct title', () => { - expect(wrapper.findComponent(GlModal).props('title')).toBe(MEMBERS_MODAL_CELEBRATE_TITLE); + expect(findModal().props('title')).toBe(MEMBERS_MODAL_CELEBRATE_TITLE); }); it('includes the correct celebration text and emoji', () => { @@ -300,11 +301,8 @@ describe('InviteMembersModal', () => { }); describe('submitting the invite form', () => { - const mockMembersApi = (code, data) => { - mock.onPost(apiPaths.GROUPS_MEMBERS).reply(code, data); - }; const mockInvitationsApi = (code, data) => { - mock.onPost(apiPaths.GROUPS_INVITATIONS).reply(code, data); + mock.onPost(GROUPS_INVITATIONS_PATH).reply(code, data); }; const expectedEmailRestrictedError = @@ -328,7 +326,7 @@ describe('InviteMembersModal', () => { await triggerMembersTokenSelect([user1, user2]); wrapper.vm.$toast = { show: jest.fn() }; - jest.spyOn(Api, 'addGroupMembersByUserId').mockResolvedValue({ data: postData }); + jest.spyOn(Api, 'inviteGroupMembers').mockResolvedValue({ data: postData }); }); describe('when triggered from regular mounting', () => { @@ -336,12 +334,8 @@ describe('InviteMembersModal', () => { clickInviteButton(); }); - it('sets isLoading on the Invite button when it is clicked', () => { - expect(findInviteButton().props('loading')).toBe(true); - }); - - it('calls Api addGroupMembersByUserId with the correct params', () => { - expect(Api.addGroupMembersByUserId).toHaveBeenCalledWith(propsData.id, postData); + it('calls Api inviteGroupMembers with the correct params', () => { + expect(Api.inviteGroupMembers).toHaveBeenCalledWith(propsData.id, postData); }); it('displays the successful toastMessage', () => { @@ -371,21 +365,9 @@ describe('InviteMembersModal', () => { await triggerMembersTokenSelect([user1]); }); - it('displays "Member already exists" api message for http status conflict', async () => { - mockMembersApi(httpStatus.CONFLICT, membersApiResponse.MEMBER_ALREADY_EXISTS); - - clickInviteButton(); - - await waitForPromises(); - - expect(membersFormGroupInvalidFeedback()).toBe('Member already exists'); - expect(findMembersSelect().props('validationState')).toBe(false); - expect(findInviteButton().props('loading')).toBe(false); - }); - describe('clearing the invalid state and message', () => { beforeEach(async () => { - mockMembersApi(httpStatus.CONFLICT, membersApiResponse.MEMBER_ALREADY_EXISTS); + mockInvitationsApi(httpStatus.CREATED, invitationsApiResponse.EMAIL_TAKEN); clickInviteButton(); @@ -393,7 +375,9 @@ describe('InviteMembersModal', () => { }); it('clears the error when the list of members to invite is cleared', async () => { - expect(membersFormGroupInvalidFeedback()).toBe('Member already exists'); + expect(membersFormGroupInvalidFeedback()).toBe( + Object.values(invitationsApiResponse.EMAIL_TAKEN.message)[0], + ); expect(findMembersSelect().props('validationState')).toBe(false); findMembersSelect().vm.$emit('clear'); @@ -414,7 +398,7 @@ describe('InviteMembersModal', () => { }); it('clears the error when the modal is hidden', async () => { - wrapper.findComponent(GlModal).vm.$emit('hide'); + findModal().vm.$emit('hidden'); await nextTick(); @@ -424,15 +408,17 @@ describe('InviteMembersModal', () => { }); it('clears the invalid state and message once the list of members to invite is cleared', async () => { - mockMembersApi(httpStatus.CONFLICT, membersApiResponse.MEMBER_ALREADY_EXISTS); + mockInvitationsApi(httpStatus.CREATED, invitationsApiResponse.EMAIL_TAKEN); clickInviteButton(); await waitForPromises(); - expect(membersFormGroupInvalidFeedback()).toBe('Member already exists'); + expect(membersFormGroupInvalidFeedback()).toBe( + Object.values(invitationsApiResponse.EMAIL_TAKEN.message)[0], + ); expect(findMembersSelect().props('validationState')).toBe(false); - expect(findInviteButton().props('loading')).toBe(false); + expect(findModal().props('actionPrimary').attributes.loading).toBe(false); findMembersSelect().vm.$emit('clear'); @@ -440,11 +426,14 @@ describe('InviteMembersModal', () => { expect(membersFormGroupInvalidFeedback()).toBe(''); expect(findMembersSelect().props('validationState')).toBe(null); - expect(findInviteButton().props('loading')).toBe(false); + expect(findModal().props('actionPrimary').attributes.loading).toBe(false); }); it('displays the generic error for http server error', async () => { - mockMembersApi(httpStatus.INTERNAL_SERVER_ERROR, 'Request failed with status code 500'); + mockInvitationsApi( + httpStatus.INTERNAL_SERVER_ERROR, + 'Request failed with status code 500', + ); clickInviteButton(); @@ -454,7 +443,7 @@ describe('InviteMembersModal', () => { }); it('displays the restricted user api message for response with bad request', async () => { - mockMembersApi(httpStatus.BAD_REQUEST, membersApiResponse.SINGLE_USER_RESTRICTED); + mockInvitationsApi(httpStatus.CREATED, invitationsApiResponse.EMAIL_RESTRICTED); clickInviteButton(); @@ -464,7 +453,7 @@ describe('InviteMembersModal', () => { }); it('displays the first part of the error when multiple existing users are restricted by email', async () => { - mockMembersApi(httpStatus.CREATED, membersApiResponse.MULTIPLE_USERS_RESTRICTED); + mockInvitationsApi(httpStatus.CREATED, invitationsApiResponse.MULTIPLE_RESTRICTED); clickInviteButton(); @@ -475,19 +464,6 @@ describe('InviteMembersModal', () => { ); expect(findMembersSelect().props('validationState')).toBe(false); }); - - it('displays an access_level error message received for the existing user', async () => { - mockMembersApi(httpStatus.BAD_REQUEST, membersApiResponse.SINGLE_USER_ACCESS_LEVEL); - - clickInviteButton(); - - await waitForPromises(); - - expect(membersFormGroupInvalidFeedback()).toBe( - 'should be greater than or equal to Owner inherited membership from group Gitlab Org', - ); - expect(findMembersSelect().props('validationState')).toBe(false); - }); }); }); @@ -508,7 +484,7 @@ describe('InviteMembersModal', () => { await triggerMembersTokenSelect([user3]); wrapper.vm.$toast = { show: jest.fn() }; - jest.spyOn(Api, 'inviteGroupMembersByEmail').mockResolvedValue({ data: postData }); + jest.spyOn(Api, 'inviteGroupMembers').mockResolvedValue({ data: postData }); }); describe('when triggered from regular mounting', () => { @@ -516,8 +492,8 @@ describe('InviteMembersModal', () => { clickInviteButton(); }); - it('calls Api inviteGroupMembersByEmail with the correct params', () => { - expect(Api.inviteGroupMembersByEmail).toHaveBeenCalledWith(propsData.id, postData); + it('calls Api inviteGroupMembers with the correct params', () => { + expect(Api.inviteGroupMembers).toHaveBeenCalledWith(propsData.id, postData); }); it('displays the successful toastMessage', () => { @@ -542,7 +518,7 @@ describe('InviteMembersModal', () => { expect(membersFormGroupInvalidFeedback()).toBe(expectedSyntaxError); expect(findMembersSelect().props('validationState')).toBe(false); - expect(findInviteButton().props('loading')).toBe(false); + expect(findModal().props('actionPrimary').attributes.loading).toBe(false); }); it('displays the restricted email error when restricted email is invited', async () => { @@ -554,23 +530,11 @@ describe('InviteMembersModal', () => { expect(membersFormGroupInvalidFeedback()).toContain(expectedEmailRestrictedError); expect(findMembersSelect().props('validationState')).toBe(false); - expect(findInviteButton().props('loading')).toBe(false); - }); - - it('displays the successful toast message when email has already been invited', async () => { - mockInvitationsApi(httpStatus.CREATED, invitationsApiResponse.EMAIL_TAKEN); - wrapper.vm.$toast = { show: jest.fn() }; - - clickInviteButton(); - - await waitForPromises(); - - expect(wrapper.vm.$toast.show).toHaveBeenCalledWith('Members were successfully added'); - expect(findMembersSelect().props('validationState')).toBe(null); + expect(findModal().props('actionPrimary').attributes.loading).toBe(false); }); it('displays the first error message when multiple emails return a restricted error message', async () => { - mockInvitationsApi(httpStatus.CREATED, invitationsApiResponse.MULTIPLE_EMAIL_RESTRICTED); + mockInvitationsApi(httpStatus.CREATED, invitationsApiResponse.MULTIPLE_RESTRICTED); clickInviteButton(); @@ -617,19 +581,17 @@ describe('InviteMembersModal', () => { format: 'json', tasks_to_be_done: [], tasks_project_id: '', + user_id: '1', + email: 'email@example.com', }; - const emailPostData = { ...postData, email: 'email@example.com' }; - const idPostData = { ...postData, user_id: '1' }; - describe('when invites are sent successfully', () => { beforeEach(async () => { createComponent(); await triggerMembersTokenSelect([user1, user3]); wrapper.vm.$toast = { show: jest.fn() }; - jest.spyOn(Api, 'inviteGroupMembersByEmail').mockResolvedValue({ data: postData }); - jest.spyOn(Api, 'addGroupMembersByUserId').mockResolvedValue({ data: postData }); + jest.spyOn(Api, 'inviteGroupMembers').mockResolvedValue({ data: postData }); }); describe('when triggered from regular mounting', () => { @@ -637,12 +599,8 @@ describe('InviteMembersModal', () => { clickInviteButton(); }); - it('calls Api inviteGroupMembersByEmail with the correct params', () => { - expect(Api.inviteGroupMembersByEmail).toHaveBeenCalledWith(propsData.id, emailPostData); - }); - - it('calls Api addGroupMembersByUserId with the correct params', () => { - expect(Api.addGroupMembersByUserId).toHaveBeenCalledWith(propsData.id, idPostData); + it('calls Api inviteGroupMembers with the correct params', () => { + expect(Api.inviteGroupMembers).toHaveBeenCalledWith(propsData.id, postData); }); it('displays the successful toastMessage', () => { @@ -655,12 +613,8 @@ describe('InviteMembersModal', () => { clickInviteButton(); - expect(Api.inviteGroupMembersByEmail).toHaveBeenCalledWith(propsData.id, { - ...emailPostData, - invite_source: '_invite_source_', - }); - expect(Api.addGroupMembersByUserId).toHaveBeenCalledWith(propsData.id, { - ...idPostData, + expect(Api.inviteGroupMembers).toHaveBeenCalledWith(propsData.id, { + ...postData, invite_source: '_invite_source_', }); }); @@ -673,7 +627,6 @@ describe('InviteMembersModal', () => { await triggerMembersTokenSelect([user1, user3]); mockInvitationsApi(httpStatus.BAD_REQUEST, invitationsApiResponse.EMAIL_INVALID); - mockMembersApi(httpStatus.OK, '200 OK'); clickInviteButton(); }); @@ -692,7 +645,7 @@ describe('InviteMembersModal', () => { await triggerMembersTokenSelect([user3]); wrapper.vm.$toast = { show: jest.fn() }; - jest.spyOn(Api, 'inviteGroupMembersByEmail').mockResolvedValue({}); + jest.spyOn(Api, 'inviteGroupMembers').mockResolvedValue({}); }); it('tracks the view for learn_gitlab source', () => { diff --git a/spec/frontend/invite_members/components/invite_modal_base_spec.js b/spec/frontend/invite_members/components/invite_modal_base_spec.js index 9e17112fb15..8355ae67f20 100644 --- a/spec/frontend/invite_members/components/invite_modal_base_spec.js +++ b/spec/frontend/invite_members/components/invite_modal_base_spec.js @@ -49,8 +49,6 @@ describe('InviteModalBase', () => { const findDatepicker = () => wrapper.findComponent(GlDatepicker); const findLink = () => wrapper.findComponent(GlLink); const findIntroText = () => wrapper.findByTestId('modal-base-intro-text').text(); - const findCancelButton = () => wrapper.findByTestId('cancel-button'); - const findInviteButton = () => wrapper.findByTestId('invite-button'); const findMembersFormGroup = () => wrapper.findByTestId('members-form-group'); describe('rendering the modal', () => { @@ -67,15 +65,21 @@ describe('InviteModalBase', () => { }); it('renders the Cancel button text correctly', () => { - expect(findCancelButton().text()).toBe(CANCEL_BUTTON_TEXT); - }); - - it('renders the Invite button text correctly', () => { - expect(findInviteButton().text()).toBe(INVITE_BUTTON_TEXT); + expect(wrapper.findComponent(GlModal).props('actionCancel')).toMatchObject({ + text: CANCEL_BUTTON_TEXT, + }); }); - it('renders the Invite button modal without isLoading', () => { - expect(findInviteButton().props('loading')).toBe(false); + it('renders the Invite button correctly', () => { + expect(wrapper.findComponent(GlModal).props('actionPrimary')).toMatchObject({ + text: INVITE_BUTTON_TEXT, + attributes: { + variant: 'confirm', + disabled: false, + loading: false, + 'data-qa-selector': 'invite_button', + }, + }); }); describe('rendering the access levels dropdown', () => { @@ -114,7 +118,7 @@ describe('InviteModalBase', () => { isLoading: true, }); - expect(findInviteButton().props('loading')).toBe(true); + expect(wrapper.findComponent(GlModal).props('actionPrimary').attributes.loading).toBe(true); }); it('with invalidFeedbackMessage, set members form group validation state', () => { diff --git a/spec/frontend/invite_members/components/members_token_select_spec.js b/spec/frontend/invite_members/components/members_token_select_spec.js index 196a716d08c..bf5564e4d63 100644 --- a/spec/frontend/invite_members/components/members_token_select_spec.js +++ b/spec/frontend/invite_members/components/members_token_select_spec.js @@ -95,7 +95,7 @@ describe('MembersTokenSelect', () => { expect(UserApi.getUsers).toHaveBeenCalledWith(searchParam, { active: true, - exclude_internal: true, + without_project_bots: true, }); expect(tokenSelector.props('hideDropdownWithNoItems')).toBe(false); }); @@ -172,7 +172,7 @@ describe('MembersTokenSelect', () => { expect(UserApi.getUsers).toHaveBeenCalledWith(searchParam, { active: true, - exclude_internal: true, + without_project_bots: true, saml_provider_id: samlProviderId, }); }); diff --git a/spec/frontend/invite_members/components/user_limit_notification_spec.js b/spec/frontend/invite_members/components/user_limit_notification_spec.js new file mode 100644 index 00000000000..c779cf2ee3f --- /dev/null +++ b/spec/frontend/invite_members/components/user_limit_notification_spec.js @@ -0,0 +1,71 @@ +import { GlAlert, GlSprintf } from '@gitlab/ui'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import UserLimitNotification from '~/invite_members/components/user_limit_notification.vue'; + +describe('UserLimitNotification', () => { + let wrapper; + + const findAlert = () => wrapper.findComponent(GlAlert); + + const createComponent = (providers = {}) => { + wrapper = shallowMountExtended(UserLimitNotification, { + provide: { + name: 'my group', + newTrialRegistrationPath: 'newTrialRegistrationPath', + purchasePath: 'purchasePath', + freeUsersLimit: 5, + membersCount: 1, + ...providers, + }, + stubs: { GlSprintf }, + }); + }; + + afterEach(() => { + wrapper.destroy(); + }); + + describe('when limit is not reached', () => { + beforeEach(() => { + createComponent(); + }); + + it('renders empty block', () => { + expect(findAlert().exists()).toBe(false); + }); + }); + + describe('when close to limit', () => { + beforeEach(() => { + createComponent({ membersCount: 3 }); + }); + + it("renders user's limit notification", () => { + const alert = findAlert(); + + expect(alert.attributes('title')).toEqual( + 'You only have space for 2 more members in my group', + ); + + expect(alert.text()).toEqual( + 'To get more members an owner of this namespace can start a trial or upgrade to a paid tier.', + ); + }); + }); + + describe('when limit is reached', () => { + beforeEach(() => { + createComponent({ membersCount: 5 }); + }); + + it("renders user's limit notification", () => { + const alert = findAlert(); + + expect(alert.attributes('title')).toEqual("You've reached your 5 members limit for my group"); + + expect(alert.text()).toEqual( + 'New members will be unable to participate. You can manage your members by removing ones you no longer need. To get more members an owner of this namespace can start a trial or upgrade to a paid tier.', + ); + }); + }); +}); |