diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2023-01-09 21:10:06 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2023-01-09 21:10:06 +0300 |
commit | 17deb2a503bb8163514fe37618bf36f75376b9ae (patch) | |
tree | f0bc3819cb3f9f19f30301191e250d069461c8d3 /spec/frontend/members | |
parent | e9de69b545c25c9cb7fd410f9bf8ba34c6bb727b (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec/frontend/members')
9 files changed, 237 insertions, 39 deletions
diff --git a/spec/frontend/members/components/action_dropdowns/leave_group_dropdown_item_spec.js b/spec/frontend/members/components/action_dropdowns/leave_group_dropdown_item_spec.js index 4b47b156cba..90f5b217007 100644 --- a/spec/frontend/members/components/action_dropdowns/leave_group_dropdown_item_spec.js +++ b/spec/frontend/members/components/action_dropdowns/leave_group_dropdown_item_spec.js @@ -4,7 +4,7 @@ import { createMockDirective, getBinding } from 'helpers/vue_mock_directive'; import LeaveGroupDropdownItem from '~/members/components/action_dropdowns/leave_group_dropdown_item.vue'; import LeaveModal from '~/members/components/modals/leave_modal.vue'; import { LEAVE_MODAL_ID } from '~/members/constants'; -import { member } from '../../mock_data'; +import { member, permissions } from '../../mock_data'; describe('LeaveGroupDropdownItem', () => { let wrapper; @@ -14,6 +14,7 @@ describe('LeaveGroupDropdownItem', () => { wrapper = shallowMount(LeaveGroupDropdownItem, { propsData: { member, + permissions, ...propsData, }, directives: { @@ -42,7 +43,7 @@ describe('LeaveGroupDropdownItem', () => { it('contains LeaveModal component', () => { const leaveModal = wrapper.findComponent(LeaveModal); - expect(leaveModal.props('member')).toEqual(member); + expect(leaveModal.props()).toEqual({ member, permissions }); }); it('binds to the LeaveModal component', () => { diff --git a/spec/frontend/members/components/action_dropdowns/remove_member_dropdown_item_spec.js b/spec/frontend/members/components/action_dropdowns/remove_member_dropdown_item_spec.js index 0ae9e257f2b..e1c498249d7 100644 --- a/spec/frontend/members/components/action_dropdowns/remove_member_dropdown_item_spec.js +++ b/spec/frontend/members/components/action_dropdowns/remove_member_dropdown_item_spec.js @@ -69,6 +69,9 @@ describe('RemoveMemberDropdownItem', () => { it('calls Vuex action to show `remove member` modal when clicked', () => { findDropdownItem().vm.$emit('click'); - expect(actions.showRemoveMemberModal).toHaveBeenCalledWith(expect.any(Object), modalData); + expect(actions.showRemoveMemberModal).toHaveBeenCalledWith(expect.any(Object), { + ...modalData, + preventRemoval: false, + }); }); }); diff --git a/spec/frontend/members/components/action_dropdowns/user_action_dropdown_spec.js b/spec/frontend/members/components/action_dropdowns/user_action_dropdown_spec.js index ae45e737581..5a2de1cac80 100644 --- a/spec/frontend/members/components/action_dropdowns/user_action_dropdown_spec.js +++ b/spec/frontend/members/components/action_dropdowns/user_action_dropdown_spec.js @@ -74,6 +74,7 @@ describe('UserActionDropdown', () => { name: member.user.name, obstacles: parseUserDeletionObstacles(member.user), }, + preventRemoval: false, }); }); @@ -120,6 +121,63 @@ describe('UserActionDropdown', () => { }); }); + describe('when user can remove but it is blocked by last owner', () => { + const permissions = { + canRemove: false, + canRemoveBlockedByLastOwner: true, + }; + + it('renders remove member dropdown', () => { + createComponent({ + permissions, + }); + + expect(findRemoveMemberDropdownItem().exists()).toBe(true); + }); + + describe('when member model type is `GroupMember`', () => { + it('passes correct message to the modal', () => { + createComponent({ + permissions, + }); + + expect(findRemoveMemberDropdownItem().props('modalMessage')).toBe( + I18N.lastGroupOwnerCannotBeRemoved, + ); + }); + }); + + describe('when member model type is `ProjectMember`', () => { + it('passes correct message to the modal', () => { + createComponent({ + member: { + ...member, + type: MEMBER_MODEL_TYPE_PROJECT_MEMBER, + }, + permissions, + }); + + expect(findRemoveMemberDropdownItem().props('modalMessage')).toBe( + I18N.personalProjectOwnerCannotBeRemoved, + ); + }); + }); + + describe('when member is the current user', () => { + it('renders leave dropdown with correct props', () => { + createComponent({ + isCurrentUser: true, + permissions, + }); + + expect(wrapper.findComponent(LeaveGroupDropdownItem).props()).toEqual({ + member, + permissions, + }); + }); + }); + }); + describe('when group member', () => { beforeEach(() => { createComponent({ diff --git a/spec/frontend/members/components/modals/leave_modal_spec.js b/spec/frontend/members/components/modals/leave_modal_spec.js index cdbabb2f646..ba587c6f0b3 100644 --- a/spec/frontend/members/components/modals/leave_modal_spec.js +++ b/spec/frontend/members/components/modals/leave_modal_spec.js @@ -1,11 +1,14 @@ import { GlModal, GlForm } from '@gitlab/ui'; -import { within } from '@testing-library/dom'; -import { mount, createWrapper } from '@vue/test-utils'; import { cloneDeep } from 'lodash'; import Vue, { nextTick } from 'vue'; import Vuex from 'vuex'; +import { mountExtended, extendedWrapper } from 'helpers/vue_test_utils_helper'; import LeaveModal from '~/members/components/modals/leave_modal.vue'; -import { LEAVE_MODAL_ID, MEMBER_TYPES } from '~/members/constants'; +import { + LEAVE_MODAL_ID, + MEMBER_TYPES, + MEMBER_MODEL_TYPE_PROJECT_MEMBER, +} from '~/members/constants'; import UserDeletionObstaclesList from '~/vue_shared/components/user_deletion_obstacles/user_deletion_obstacles_list.vue'; import { parseUserDeletionObstacles } from '~/vue_shared/components/user_deletion_obstacles/utils'; import { member } from '../../mock_data'; @@ -31,14 +34,17 @@ describe('LeaveModal', () => { }); }; - const createComponent = (propsData = {}, state) => { - wrapper = mount(LeaveModal, { + const createComponent = async (propsData = {}, state) => { + wrapper = mountExtended(LeaveModal, { store: createStore(state), provide: { namespace: MEMBER_TYPES.user, }, propsData: { member, + permissions: { + canRemove: true, + }, ...propsData, }, attrs: { @@ -46,39 +52,98 @@ describe('LeaveModal', () => { visible: true, }, }); + + await nextTick(); }; - const findModal = () => wrapper.findComponent(GlModal); + const findModal = () => extendedWrapper(wrapper.findComponent(GlModal)); const findForm = () => findModal().findComponent(GlForm); const findUserDeletionObstaclesList = () => findModal().findComponent(UserDeletionObstaclesList); - const getByText = (text, options) => - createWrapper(within(findModal().element).getByText(text, options)); - - beforeEach(async () => { - createComponent(); - await nextTick(); - }); - afterEach(() => { wrapper.destroy(); }); - it('sets modal ID', () => { + it('sets modal ID', async () => { + await createComponent(); + expect(findModal().props('modalId')).toBe(LEAVE_MODAL_ID); }); - it('displays modal title', () => { - expect(getByText(`Leave "${member.source.fullName}"`).exists()).toBe(true); + describe('when leave is allowed', () => { + it('displays modal title', async () => { + await createComponent(); + + expect(findModal().findByText(`Leave "${member.source.fullName}"`).exists()).toBe(true); + }); + + it('displays modal body', async () => { + await createComponent(); + + expect( + findModal() + .findByText(`Are you sure you want to leave "${member.source.fullName}"?`) + .exists(), + ).toBe(true); + }); }); - it('displays modal body', () => { - expect(getByText(`Are you sure you want to leave "${member.source.fullName}"?`).exists()).toBe( - true, - ); + describe('when leave is blocked by last owner', () => { + const permissions = { + canRemove: false, + canRemoveBlockedByLastOwner: true, + }; + + it('does not show primary action button', async () => { + await createComponent({ + permissions, + }); + + expect(findModal().props('actionPrimary')).toBe(null); + }); + + it('displays modal title', async () => { + await createComponent({ + permissions, + }); + + expect(findModal().findByText(`Cannot leave "${member.source.fullName}"`).exists()).toBe( + true, + ); + }); + + describe('when member model type is `GroupMember`', () => { + it('displays modal body', async () => { + await createComponent({ + permissions, + }); + + expect( + findModal().findByText(LeaveModal.i18n.preventedBodyGroupMemberModelType).exists(), + ).toBe(true); + }); + }); + + describe('when member model type is `ProjectMember`', () => { + it('displays modal body', async () => { + await createComponent({ + member: { + ...member, + type: MEMBER_MODEL_TYPE_PROJECT_MEMBER, + }, + permissions, + }); + + expect( + findModal().findByText(LeaveModal.i18n.preventedBodyProjectMemberModelType).exists(), + ).toBe(true); + }); + }); }); - it('displays form with correct action and inputs', () => { + it('displays form with correct action and inputs', async () => { + await createComponent(); + const form = findForm(); expect(form.attributes('action')).toBe('/groups/foo-bar/-/group_members/leave'); @@ -89,7 +154,9 @@ describe('LeaveModal', () => { }); describe('User deletion obstacles list', () => { - it("displays obstacles list when member's user is part of on-call management", () => { + it("displays obstacles list when member's user is part of on-call management", async () => { + await createComponent(); + const obstaclesList = findUserDeletionObstaclesList(); expect(obstaclesList.exists()).toBe(true); expect(obstaclesList.props()).toMatchObject({ @@ -105,17 +172,18 @@ describe('LeaveModal', () => { delete memberWithoutOncall.user.oncallSchedules; delete memberWithoutOncall.user.escalationPolicies; - createComponent({ member: memberWithoutOncall }); - await nextTick(); + await createComponent({ member: memberWithoutOncall }); expect(findUserDeletionObstaclesList().exists()).toBe(false); }); }); - it('submits the form when "Leave" button is clicked', () => { + it('submits the form when "Leave" button is clicked', async () => { + await createComponent(); + const submitSpy = jest.spyOn(findForm().element, 'submit'); - getByText('Leave').trigger('click'); + findModal().findByText('Leave').trigger('click'); expect(submitSpy).toHaveBeenCalled(); diff --git a/spec/frontend/members/components/modals/remove_member_modal_spec.js b/spec/frontend/members/components/modals/remove_member_modal_spec.js index a9ff95dd948..47a03b5083a 100644 --- a/spec/frontend/members/components/modals/remove_member_modal_spec.js +++ b/spec/frontend/members/components/modals/remove_member_modal_spec.js @@ -137,4 +137,28 @@ describe('RemoveMemberModal', () => { }); }, ); + + describe('when removal is prevented', () => { + const message = + 'A group must have at least one owner. To remove the member, assign a new owner.'; + + beforeEach(() => { + createComponent({ + actionText: 'Remove member', + memberModelType: MEMBER_MODEL_TYPE_GROUP_MEMBER, + isAccessRequest: false, + isInvite: false, + message, + preventRemoval: true, + }); + }); + + it('does not show primary action button', () => { + expect(findGlModal().props('actionPrimary')).toBe(null); + }); + + it('only shows the message', () => { + expect(findGlModal().text()).toBe(message); + }); + }); }); diff --git a/spec/frontend/members/components/table/members_table_cell_spec.js b/spec/frontend/members/components/table/members_table_cell_spec.js index 0b0140b0cdb..ac5d83d028d 100644 --- a/spec/frontend/members/components/table/members_table_cell_spec.js +++ b/spec/frontend/members/components/table/members_table_cell_spec.js @@ -3,6 +3,7 @@ import Vue from 'vue'; import Vuex from 'vuex'; import MembersTableCell from '~/members/components/table/members_table_cell.vue'; import { MEMBER_TYPES } from '~/members/constants'; +import { canRemoveBlockedByLastOwner } from '~/members/utils'; import { member as memberMock, directMember, @@ -12,6 +13,11 @@ import { accessRequest, } from '../../mock_data'; +jest.mock('~/members/utils', () => ({ + ...jest.requireActual('~/members/utils'), + canRemoveBlockedByLastOwner: jest.fn().mockImplementation(() => true), +})); + describe('MembersTableCell', () => { const WrappedComponent = { props: { @@ -55,6 +61,7 @@ describe('MembersTableCell', () => { provide: { sourceId: 1, currentUserId: 1, + canManageMembers: true, }, scopedSlots: { default: ` @@ -179,6 +186,15 @@ describe('MembersTableCell', () => { }); }); + describe('canRemoveBlockedByLastOwner', () => { + it('calls util and returns value', () => { + createComponentWithDirectMember(); + + expect(canRemoveBlockedByLastOwner).toHaveBeenCalledWith(directMember, true); + expect(findWrappedComponent().props('permissions').canRemoveBlockedByLastOwner).toBe(true); + }); + }); + describe('canResend', () => { describe('when member type is `invite`', () => { it('returns `true` when `canResend` is `true`', () => { diff --git a/spec/frontend/members/components/table/members_table_spec.js b/spec/frontend/members/components/table/members_table_spec.js index 3165173ce21..1d18026a410 100644 --- a/spec/frontend/members/components/table/members_table_spec.js +++ b/spec/frontend/members/components/table/members_table_spec.js @@ -63,6 +63,7 @@ describe('MembersTable', () => { provide: { sourceId: 1, currentUserId: 1, + canManageMembers: true, namespace: MEMBER_TYPES.invite, ...provide, }, @@ -200,16 +201,23 @@ describe('MembersTable', () => { canRemove: true, }; + const memberCanRemoveBlockedLastOwner = { + ...directMember, + canRemove: false, + isLastOwner: true, + }; + const memberNoPermissions = { ...memberMock, id: 2, }; describe.each` - permission | members - ${'canUpdate'} | ${[memberNoPermissions, memberCanUpdate]} - ${'canRemove'} | ${[memberNoPermissions, memberCanRemove]} - ${'canResend'} | ${[memberNoPermissions, invite]} + permission | members + ${'canUpdate'} | ${[memberNoPermissions, memberCanUpdate]} + ${'canRemove'} | ${[memberNoPermissions, memberCanRemove]} + ${'canRemoveBlockedByLastOwner'} | ${[memberNoPermissions, memberCanRemoveBlockedLastOwner]} + ${'canResend'} | ${[memberNoPermissions, invite]} `('when one of the members has $permission permissions', ({ members }) => { it('renders the "Actions" field', () => { createComponent({ members, tableFields: ['actions'] }); @@ -228,10 +236,11 @@ describe('MembersTable', () => { }); describe.each` - permission | members - ${'canUpdate'} | ${[memberMock]} - ${'canRemove'} | ${[memberMock]} - ${'canResend'} | ${[{ ...invite, invite: { ...invite.invite, canResend: false } }]} + permission | members + ${'canUpdate'} | ${[memberMock]} + ${'canRemove'} | ${[memberMock]} + ${'canRemoveBlockedByLastOwner'} | ${[memberMock]} + ${'canResend'} | ${[{ ...invite, invite: { ...invite.invite, canResend: false } }]} `('when none of the members have $permission permissions', ({ members }) => { it('does not render the "Actions" field', () => { createComponent({ members, tableFields: ['actions'] }); diff --git a/spec/frontend/members/mock_data.js b/spec/frontend/members/mock_data.js index 3f88cb7b386..7ab642c84a7 100644 --- a/spec/frontend/members/mock_data.js +++ b/spec/frontend/members/mock_data.js @@ -17,7 +17,7 @@ export const member = { fullName: 'Foo Bar', webUrl: 'https://gitlab.com/groups/foo-bar', }, - type: 'GroupMember', + type: MEMBER_MODEL_TYPE_GROUP_MEMBER, state: MEMBER_STATE_CREATED, user: { id: 123, @@ -131,3 +131,10 @@ export const dataAttribute = JSON.stringify({ source_id: 234, can_manage_members: true, }); + +export const permissions = { + canRemove: true, + canRemoveBlockedByLastOwner: false, + canResend: true, + canUpdate: true, +}; diff --git a/spec/frontend/members/utils_spec.js b/spec/frontend/members/utils_spec.js index 8bef2096a2a..9d52c789db2 100644 --- a/spec/frontend/members/utils_spec.js +++ b/spec/frontend/members/utils_spec.js @@ -13,6 +13,7 @@ import { isDirectMember, isCurrentUser, canRemove, + canRemoveBlockedByLastOwner, canResend, canUpdate, canOverride, @@ -129,6 +130,17 @@ describe('Members Utils', () => { }); }); + describe('canRemoveBlockedByLastOwner', () => { + it.each` + member | canManageMembers | expected + ${{ ...directMember, isLastOwner: true }} | ${true} | ${true} + ${{ ...inheritedMember, isLastOwner: false }} | ${true} | ${false} + ${{ ...directMember, isLastOwner: true }} | ${false} | ${false} + `('returns $expected', ({ member, canManageMembers, expected }) => { + expect(canRemoveBlockedByLastOwner(member, canManageMembers)).toBe(expected); + }); + }); + describe('canResend', () => { it.each` member | expected |