diff options
Diffstat (limited to 'spec/frontend/invite_members')
7 files changed, 140 insertions, 54 deletions
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 4136de75545..358d70d8117 100644 --- a/spec/frontend/invite_members/components/invite_groups_modal_spec.js +++ b/spec/frontend/invite_members/components/invite_groups_modal_spec.js @@ -77,6 +77,16 @@ describe('InviteGroupsModal', () => { const clickInviteButton = emitClickFromModal('invite-modal-submit'); const clickCancelButton = emitClickFromModal('invite-modal-cancel'); + describe('passes correct props to InviteModalBase', () => { + it('set accessLevel', () => { + createInviteGroupToProjectWrapper(); + + expect(findBase().props('accessLevels')).toMatchObject({ + validRoles: propsData.accessLevels, + }); + }); + }); + describe('displaying the correct introText and form group description', () => { describe('when inviting to a project', () => { it('includes the correct type, and formatted intro text', () => { 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 19b7fad5fc8..ad3174b8946 100644 --- a/spec/frontend/invite_members/components/invite_members_modal_spec.js +++ b/spec/frontend/invite_members/components/invite_members_modal_spec.js @@ -128,6 +128,7 @@ describe('InviteMembersModal', () => { }); const findModal = () => wrapper.findComponent(GlModal); + const findBase = () => wrapper.findComponent(InviteModalBase); const findIntroText = () => wrapper.findByTestId('modal-base-intro-text').text(); const findEmptyInvitesAlert = () => wrapper.findByTestId('empty-invites-alert'); const findMemberErrorAlert = () => wrapper.findByTestId('alert-member-error'); @@ -168,6 +169,22 @@ describe('InviteMembersModal', () => { await nextTick(); }; + describe('passes correct props to InviteModalBase', () => { + it('set defaultMemberRoleId', () => { + createInviteMembersToProjectWrapper(); + + expect(findBase().props('defaultMemberRoleId')).toBeNull(); + }); + + it('set accessLevel', () => { + createInviteMembersToProjectWrapper(); + + expect(findBase().props('accessLevels')).toMatchObject({ + validRoles: propsData.accessLevels, + }); + }); + }); + describe('rendering with tracking considerations', () => { describe('when inviting to a project', () => { describe('when inviting members', () => { diff --git a/spec/frontend/invite_members/components/invite_members_trigger_spec.js b/spec/frontend/invite_members/components/invite_members_trigger_spec.js index 58c40a49b3c..f14d24538d8 100644 --- a/spec/frontend/invite_members/components/invite_members_trigger_spec.js +++ b/spec/frontend/invite_members/components/invite_members_trigger_spec.js @@ -4,7 +4,6 @@ import InviteMembersTrigger from '~/invite_members/components/invite_members_tri import eventHub from '~/invite_members/event_hub'; import { TRIGGER_ELEMENT_BUTTON, - TRIGGER_DEFAULT_QA_SELECTOR, TRIGGER_ELEMENT_WITH_EMOJI, TRIGGER_ELEMENT_DROPDOWN_WITH_EMOJI, TRIGGER_ELEMENT_DISCLOSURE_DROPDOWN, @@ -66,18 +65,6 @@ describe.each(triggerItems)('with triggerElement as %s', (triggerItem) => { expect(findButton().text()).toBe(displayText); }); - - it('uses the default qa selector value', () => { - createComponent(); - - expect(findButton().attributes('data-qa-selector')).toBe(TRIGGER_DEFAULT_QA_SELECTOR); - }); - - it('sets the qa selector value', () => { - createComponent({ qaSelector: '_qaSelector_' }); - - expect(findButton().attributes('data-qa-selector')).toBe('_qaSelector_'); - }); }); describe('clicking the link', () => { 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 e70c83a424e..c26d1d921a5 100644 --- a/spec/frontend/invite_members/components/invite_modal_base_spec.js +++ b/spec/frontend/invite_members/components/invite_modal_base_spec.js @@ -1,5 +1,5 @@ import { - GlFormSelect, + GlCollapsibleListbox, GlDatepicker, GlFormGroup, GlLink, @@ -7,9 +7,14 @@ import { GlModal, GlIcon, } from '@gitlab/ui'; +import { nextTick } from 'vue'; import { stubComponent } from 'helpers/stub_component'; import { mockTracking, unmockTracking } from 'helpers/tracking_helper'; -import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import { + mountExtended, + shallowMountExtended, + extendedWrapper, +} from 'helpers/vue_test_utils_helper'; import InviteModalBase from '~/invite_members/components/invite_modal_base.vue'; import ContentTransition from '~/vue_shared/components/content_transition.vue'; @@ -31,7 +36,7 @@ describe('InviteModalBase', () => { ? {} : { ContentTransition, - GlFormSelect: true, + GlCollapsibleListbox: true, GlSprintf, GlFormGroup: stubComponent(GlFormGroup, { props: ['state', 'invalidFeedback'], @@ -41,6 +46,7 @@ describe('InviteModalBase', () => { wrapper = mountFn(InviteModalBase, { propsData: { ...propsData, + accessLevels: { validRoles: propsData.accessLevels }, ...props, }, stubs: { @@ -54,8 +60,8 @@ describe('InviteModalBase', () => { }); }; - const findFormSelect = () => wrapper.findComponent(GlFormSelect); - const findFormSelectOptions = () => findFormSelect().findAllComponents('option'); + const findCollapsibleListbox = () => extendedWrapper(wrapper.findComponent(GlCollapsibleListbox)); + const findCollapsibleListboxOptions = () => findCollapsibleListbox().findAllByRole('option'); const findDatepicker = () => wrapper.findComponent(GlDatepicker); const findLink = () => wrapper.findComponent(GlLink); const findIcon = () => wrapper.findComponent(GlIcon); @@ -91,7 +97,6 @@ describe('InviteModalBase', () => { const actionButton = findActionButton(); expect(actionButton.text()).toBe(INVITE_BUTTON_TEXT); - expect(actionButton.attributes('data-qa-selector')).toBe('invite_button'); expect(actionButton.props()).toMatchObject({ variant: 'confirm', @@ -103,17 +108,47 @@ describe('InviteModalBase', () => { describe('rendering the access levels dropdown', () => { beforeEach(() => { createComponent({ + props: { isLoadingRoles: true }, mountFn: mountExtended, }); }); + it('passes `isLoadingRoles` prop to the dropdown', () => { + expect(findCollapsibleListbox().props('loading')).toBe(true); + }); + it('sets the default dropdown text to the default access level name', () => { - expect(findFormSelect().exists()).toBe(true); - expect(findFormSelect().element.value).toBe('10'); + expect(findCollapsibleListbox().exists()).toBe(true); + const option = findCollapsibleListbox().find('[aria-selected]'); + expect(option.text()).toBe('Reporter'); + }); + + it('updates the selection base on changes in the dropdown', async () => { + wrapper.setProps({ accessLevels: { validRoles: [] } }); + expect(findCollapsibleListbox().props('selected')).not.toHaveLength(0); + await nextTick(); + + expect(findCollapsibleListboxOptions()).toHaveLength(0); + expect(findCollapsibleListbox().props('selected')).toHaveLength(0); + }); + + it('reset the dropdown to the default option', async () => { + const developerOption = findCollapsibleListboxOptions().at(2); + await developerOption.trigger('click'); + + let option; + option = findCollapsibleListbox().find('[aria-selected]'); + expect(option.text()).toBe('Developer'); + + // Reset the dropdown by clicking cancel button + await findCancelButton().trigger('click'); + + option = findCollapsibleListbox().find('[aria-selected]'); + expect(option.text()).toBe('Reporter'); }); it('renders dropdown items for each accessLevel', () => { - expect(findFormSelectOptions()).toHaveLength(5); + expect(findCollapsibleListboxOptions()).toHaveLength(5); }); }); @@ -211,7 +246,7 @@ describe('InviteModalBase', () => { it('renders correct blocks', () => { expect(findIcon().exists()).toBe(false); expect(findDisabledInput().exists()).toBe(false); - expect(findFormSelect().exists()).toBe(true); + expect(findCollapsibleListbox().exists()).toBe(true); expect(findDatepicker().exists()).toBe(true); expect(wrapper.findComponent(GlModal).text()).toMatch(textRegex); }); 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 a4b8a8b0197..a2b21367388 100644 --- a/spec/frontend/invite_members/components/members_token_select_spec.js +++ b/spec/frontend/invite_members/components/members_token_select_spec.js @@ -6,23 +6,32 @@ import waitForPromises from 'helpers/wait_for_promises'; import * as UserApi from '~/api/user_api'; import MembersTokenSelect from '~/invite_members/components/members_token_select.vue'; import { VALID_TOKEN_BACKGROUND, INVALID_TOKEN_BACKGROUND } from '~/invite_members/constants'; +import * as Sentry from '~/sentry/sentry_browser_wrapper'; const label = 'testgroup'; const placeholder = 'Search for a member'; +const rootGroupId = '31'; const user1 = { id: 1, name: 'John Smith', username: 'one_1', avatar_url: '' }; const user2 = { id: 2, name: 'Jane Doe', username: 'two_2', avatar_url: '' }; const allUsers = [user1, user2]; +const handleEnterSpy = jest.fn(); -const createComponent = (props) => { +const createComponent = (props = {}, glFeatures = {}) => { return shallowMount(MembersTokenSelect, { propsData: { ariaLabelledby: label, invalidMembers: {}, placeholder, + rootGroupId, ...props, }, + provide: { glFeatures }, stubs: { - GlTokenSelector: stubComponent(GlTokenSelector), + GlTokenSelector: stubComponent(GlTokenSelector, { + methods: { + handleEnter: handleEnterSpy, + }, + }), }, }); }; @@ -84,23 +93,11 @@ describe('MembersTokenSelect', () => { wrapper = createComponent(); }); - describe('when input is focused for the first time (modal auto-focus)', () => { - it('does not call the API', async () => { - findTokenSelector().vm.$emit('focus'); - - await waitForPromises(); - - expect(UserApi.getUsers).not.toHaveBeenCalled(); - }); - }); - describe('when input is manually focused', () => { it('calls the API and sets dropdown items as request result', async () => { const tokenSelector = findTokenSelector(); tokenSelector.vm.$emit('focus'); - tokenSelector.vm.$emit('blur'); - tokenSelector.vm.$emit('focus'); await waitForPromises(); @@ -173,6 +170,29 @@ describe('MembersTokenSelect', () => { }); }); }); + + describe('when API search fails', () => { + beforeEach(() => { + jest.spyOn(Sentry, 'captureException'); + jest.spyOn(UserApi, 'getUsers').mockRejectedValue('error'); + }); + + it('reports to sentry', async () => { + tokenSelector.vm.$emit('text-input', 'Den'); + + await waitForPromises(); + + expect(Sentry.captureException).toHaveBeenCalledWith('error'); + }); + }); + + it('allows tab to function as enter', () => { + tokenSelector.vm.$emit('text-input', 'username'); + + tokenSelector.vm.$emit('keydown', new KeyboardEvent('keydown', { key: 'Tab' })); + + expect(handleEnterSpy).toHaveBeenCalled(); + }); }); describe('when user is selected', () => { @@ -215,31 +235,45 @@ describe('MembersTokenSelect', () => { }); }); - describe('when component is mounted for a group using a saml provider', () => { + describe('when component is mounted for a group using a SAML provider', () => { const searchParam = 'name'; - const samlProviderId = 123; - let resolveApiRequest; beforeEach(() => { - jest.spyOn(UserApi, 'getUsers').mockImplementation( - () => - new Promise((resolve) => { - resolveApiRequest = resolve; - }), - ); + jest.spyOn(UserApi, 'getGroupUsers').mockResolvedValue({ data: allUsers }); - wrapper = createComponent({ filterId: samlProviderId, usersFilter: 'saml_provider_id' }); + wrapper = createComponent({ usersFilter: 'saml_provider_id' }, { groupUserSaml: true }); findTokenSelector().vm.$emit('text-input', searchParam); }); - it('calls the API with the saml provider ID param', () => { - resolveApiRequest({ data: allUsers }); - - expect(UserApi.getUsers).toHaveBeenCalledWith(searchParam, { + it('calls the group API with correct parameters', () => { + expect(UserApi.getGroupUsers).toHaveBeenCalledWith(searchParam, rootGroupId, { active: true, - without_project_bots: true, - saml_provider_id: samlProviderId, + include_saml_users: true, + include_service_accounts: true, + }); + }); + }); + + describe('when group_user_saml feature flag is disabled', () => { + describe('when component is mounted for a group using a SAML provider', () => { + const searchParam = 'name'; + const samlProviderId = 123; + + beforeEach(() => { + jest.spyOn(UserApi, 'getUsers').mockResolvedValue({ data: allUsers }); + + wrapper = createComponent({ filterId: samlProviderId, usersFilter: 'saml_provider_id' }); + + findTokenSelector().vm.$emit('text-input', searchParam); + }); + + it('calls the API with the saml provider ID param', () => { + expect(UserApi.getUsers).toHaveBeenCalledWith(searchParam, { + active: true, + without_project_bots: true, + saml_provider_id: samlProviderId, + }); }); }); }); diff --git a/spec/frontend/invite_members/mock_data/member_modal.js b/spec/frontend/invite_members/mock_data/member_modal.js index 8cde13bf69c..0c0e669b894 100644 --- a/spec/frontend/invite_members/mock_data/member_modal.js +++ b/spec/frontend/invite_members/mock_data/member_modal.js @@ -40,6 +40,7 @@ export const user6 = { export const postData = { user_id: `${user1.id},${user2.id}`, access_level: propsData.defaultAccessLevel, + member_role_id: null, expires_at: undefined, invite_source: inviteSource, format: 'json', @@ -47,6 +48,7 @@ export const postData = { export const emailPostData = { access_level: propsData.defaultAccessLevel, + member_role_id: null, expires_at: undefined, email: `${user3.name}`, invite_source: inviteSource, @@ -55,6 +57,7 @@ export const emailPostData = { export const singleUserPostData = { access_level: propsData.defaultAccessLevel, + member_role_id: null, expires_at: undefined, user_id: `${user1.id}`, email: `${user3.name}`, diff --git a/spec/frontend/invite_members/mock_data/modal_base.js b/spec/frontend/invite_members/mock_data/modal_base.js index 565e8d4df1e..c44e890da3d 100644 --- a/spec/frontend/invite_members/mock_data/modal_base.js +++ b/spec/frontend/invite_members/mock_data/modal_base.js @@ -3,7 +3,7 @@ export const propsData = { modalId: '_modal_id_', name: '_name_', accessLevels: { Guest: 10, Reporter: 20, Developer: 30, Maintainer: 40, Owner: 50 }, - defaultAccessLevel: 10, + defaultAccessLevel: 20, helpLink: 'https://example.com', labelIntroText: '_label_intro_text_', labelSearchField: '_label_search_field_', |