diff options
Diffstat (limited to 'spec/frontend/members/components/table/max_role_spec.js')
-rw-r--r-- | spec/frontend/members/components/table/max_role_spec.js | 213 |
1 files changed, 213 insertions, 0 deletions
diff --git a/spec/frontend/members/components/table/max_role_spec.js b/spec/frontend/members/components/table/max_role_spec.js new file mode 100644 index 00000000000..75e1e05afb1 --- /dev/null +++ b/spec/frontend/members/components/table/max_role_spec.js @@ -0,0 +1,213 @@ +import { GlBadge, GlCollapsibleListbox, GlListboxItem } from '@gitlab/ui'; +import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils'; +import { mount } from '@vue/test-utils'; +import Vue, { nextTick } from 'vue'; +// eslint-disable-next-line no-restricted-imports +import Vuex from 'vuex'; +import * as Sentry from '~/sentry/sentry_browser_wrapper'; +import waitForPromises from 'helpers/wait_for_promises'; +import MaxRole from '~/members/components/table/max_role.vue'; +import { MEMBER_TYPES } from '~/members/constants'; +import { guestOverageConfirmAction } from 'ee_else_ce/members/guest_overage_confirm_action'; +import { member } from '../../mock_data'; + +Vue.use(Vuex); + +jest.mock('ee_else_ce/members/guest_overage_confirm_action'); +jest.mock('~/sentry/sentry_browser_wrapper'); + +guestOverageConfirmAction.mockReturnValue(true); + +describe('MaxRole', () => { + let wrapper; + let actions; + const $toast = { + show: jest.fn(), + }; + + const createStore = ({ updateMemberRoleReturn = Promise.resolve() } = {}) => { + actions = { + updateMemberRole: jest.fn(() => updateMemberRoleReturn), + }; + + return new Vuex.Store({ + modules: { + [MEMBER_TYPES.user]: { namespaced: true, actions }, + }, + }); + }; + + const createComponent = (propsData = {}, store = createStore()) => { + wrapper = mount(MaxRole, { + provide: { + namespace: MEMBER_TYPES.user, + group: { + name: 'groupname', + path: '/grouppath/', + }, + }, + propsData: { + member, + permissions: { + canUpdate: true, + }, + ...propsData, + }, + store, + mocks: { + $toast, + }, + }); + }; + + const findBadge = () => wrapper.findComponent(GlBadge); + const findListbox = () => wrapper.findComponent(GlCollapsibleListbox); + const findListboxItems = () => wrapper.findAllComponents(GlListboxItem); + const findListboxItemByText = (text) => + findListboxItems().wrappers.find((item) => item.text() === text); + + beforeEach(() => { + gon.features = { showOverageOnRolePromotion: true }; + }); + + describe('when member can not be updated', () => { + it('renders a badge instead of a collapsible listbox', () => { + createComponent({ + permissions: { + canUpdate: false, + }, + }); + + expect(findBadge().text()).toBe('Owner'); + }); + }); + + it('has correct header text props', () => { + createComponent(); + expect(findListbox().props('headerText')).toBe('Change role'); + }); + + it('has items prop with all valid roles', () => { + createComponent(); + const roles = findListboxItems().wrappers.map((item) => item.text()); + expect(roles).toEqual(Object.keys(member.validRoles)); + }); + + describe('when listbox is open', () => { + beforeEach(async () => { + createComponent(); + + await findListbox().vm.$emit('click'); + }); + + it('sets dropdown toggle and checks selected role', () => { + expect(findListbox().find('[aria-selected=true]').text()).toBe('Owner'); + }); + + describe('when dropdown item is selected', () => { + it('does nothing if the item selected was already selected', async () => { + await findListboxItemByText('Owner').trigger('click'); + + expect(actions.updateMemberRole).not.toHaveBeenCalled(); + }); + + it('calls `updateMemberRole` Vuex action', async () => { + await findListboxItemByText('Developer').trigger('click'); + + expect(actions.updateMemberRole).toHaveBeenCalledWith(expect.any(Object), { + memberId: member.id, + accessLevel: 30, + memberRoleId: null, + }); + }); + + describe('when updateMemberRole is successful', () => { + it('displays toast', async () => { + await findListboxItemByText('Developer').trigger('click'); + + await waitForPromises(); + + expect($toast.show).toHaveBeenCalledWith('Role updated successfully.'); + }); + + it('puts dropdown in loading state while waiting for `updateMemberRole` to resolve', async () => { + await findListboxItemByText('Developer').trigger('click'); + + expect(findListbox().props('loading')).toBe(true); + }); + + it('enables dropdown after `updateMemberRole` resolves', async () => { + await findListboxItemByText('Developer').trigger('click'); + + await waitForPromises(); + + expect(findListbox().props('disabled')).toBe(false); + }); + + it('does not log error to Sentry', async () => { + await findListboxItemByText('Developer').trigger('click'); + + await waitForPromises(); + + expect(Sentry.captureException).not.toHaveBeenCalled(); + }); + }); + + describe('when updateMemberRole is not successful', () => { + const reason = 'Rejected ☹️'; + + beforeEach(() => { + createComponent({}, createStore({ updateMemberRoleReturn: Promise.reject(reason) })); + }); + + it('does not display toast', async () => { + await findListboxItemByText('Developer').trigger('click'); + + await waitForPromises(); + + expect($toast.show).not.toHaveBeenCalled(); + }); + + it('puts dropdown in loading state while waiting for `updateMemberRole` to resolve', async () => { + await findListboxItemByText('Developer').trigger('click'); + + expect(findListbox().props('loading')).toBe(true); + }); + + it('enables dropdown after `updateMemberRole` resolves', async () => { + await findListboxItemByText('Developer').trigger('click'); + + await waitForPromises(); + + expect(findListbox().props('disabled')).toBe(false); + }); + + it('logs error to Sentry', async () => { + await findListboxItemByText('Developer').trigger('click'); + + await waitForPromises(); + + expect(Sentry.captureException).toHaveBeenCalledWith(reason); + }); + }); + }); + }); + + it('sets the dropdown alignment to right on mobile', async () => { + jest.spyOn(bp, 'isDesktop').mockReturnValue(false); + createComponent(); + + await nextTick(); + + expect(findListbox().props('placement')).toBe('right'); + }); + + it('sets the dropdown alignment to left on desktop', async () => { + jest.spyOn(bp, 'isDesktop').mockReturnValue(true); + createComponent(); + + await nextTick(); + + expect(findListbox().props('placement')).toBe('left'); + }); +}); |