summaryrefslogtreecommitdiff
path: root/spec/frontend/members/components
diff options
context:
space:
mode:
Diffstat (limited to 'spec/frontend/members/components')
-rw-r--r--spec/frontend/members/components/action_buttons/access_request_action_buttons_spec.js1
-rw-r--r--spec/frontend/members/components/action_buttons/invite_action_buttons_spec.js2
-rw-r--r--spec/frontend/members/components/action_buttons/leave_button_spec.js59
-rw-r--r--spec/frontend/members/components/action_buttons/remove_member_button_spec.js18
-rw-r--r--spec/frontend/members/components/action_buttons/user_action_buttons_spec.js161
-rw-r--r--spec/frontend/members/components/action_dropdowns/leave_group_dropdown_item_spec.js54
-rw-r--r--spec/frontend/members/components/action_dropdowns/remove_member_dropdown_item_spec.js77
-rw-r--r--spec/frontend/members/components/action_dropdowns/user_action_dropdown_spec.js220
-rw-r--r--spec/frontend/members/components/modals/leave_modal_spec.js122
-rw-r--r--spec/frontend/members/components/modals/remove_member_modal_spec.js44
-rw-r--r--spec/frontend/members/components/table/__snapshots__/member_activity_spec.js.snap61
-rw-r--r--spec/frontend/members/components/table/created_at_spec.js19
-rw-r--r--spec/frontend/members/components/table/member_action_buttons_spec.js4
-rw-r--r--spec/frontend/members/components/table/member_activity_spec.js40
-rw-r--r--spec/frontend/members/components/table/member_source_spec.js94
-rw-r--r--spec/frontend/members/components/table/members_table_cell_spec.js16
-rw-r--r--spec/frontend/members/components/table/members_table_spec.js45
-rw-r--r--spec/frontend/members/components/table/role_dropdown_spec.js66
18 files changed, 761 insertions, 342 deletions
diff --git a/spec/frontend/members/components/action_buttons/access_request_action_buttons_spec.js b/spec/frontend/members/components/action_buttons/access_request_action_buttons_spec.js
index df5c884f42e..b94964dc482 100644
--- a/spec/frontend/members/components/action_buttons/access_request_action_buttons_spec.js
+++ b/spec/frontend/members/components/action_buttons/access_request_action_buttons_spec.js
@@ -38,7 +38,6 @@ describe('AccessRequestActionButtons', () => {
title: 'Deny access',
isAccessRequest: true,
isInvite: false,
- icon: 'close',
});
});
diff --git a/spec/frontend/members/components/action_buttons/invite_action_buttons_spec.js b/spec/frontend/members/components/action_buttons/invite_action_buttons_spec.js
index ea819b4fb83..68009708c99 100644
--- a/spec/frontend/members/components/action_buttons/invite_action_buttons_spec.js
+++ b/spec/frontend/members/components/action_buttons/invite_action_buttons_spec.js
@@ -39,12 +39,10 @@ describe('InviteActionButtons', () => {
it('sets props correctly', () => {
expect(findRemoveMemberButton().props()).toMatchObject({
memberId: member.id,
- memberType: null,
message: `Are you sure you want to revoke the invitation for ${member.invite.email} to join "${member.source.fullName}"`,
title: 'Revoke invite',
isAccessRequest: false,
isInvite: true,
- icon: 'remove',
});
});
});
diff --git a/spec/frontend/members/components/action_buttons/leave_button_spec.js b/spec/frontend/members/components/action_buttons/leave_button_spec.js
deleted file mode 100644
index ecfbf4460a6..00000000000
--- a/spec/frontend/members/components/action_buttons/leave_button_spec.js
+++ /dev/null
@@ -1,59 +0,0 @@
-import { GlButton } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
-import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
-import LeaveButton from '~/members/components/action_buttons/leave_button.vue';
-import LeaveModal from '~/members/components/modals/leave_modal.vue';
-import { LEAVE_MODAL_ID } from '~/members/constants';
-import { member } from '../../mock_data';
-
-describe('LeaveButton', () => {
- let wrapper;
-
- const createComponent = (propsData = {}) => {
- wrapper = shallowMount(LeaveButton, {
- propsData: {
- member,
- ...propsData,
- },
- directives: {
- GlTooltip: createMockDirective(),
- GlModal: createMockDirective(),
- },
- });
- };
-
- const findButton = () => wrapper.findComponent(GlButton);
-
- beforeEach(() => {
- createComponent();
- });
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- it('displays a tooltip', () => {
- const button = findButton();
-
- expect(getBinding(button.element, 'gl-tooltip')).not.toBeUndefined();
- expect(button.attributes('title')).toBe('Leave');
- });
-
- it('sets `aria-label` attribute', () => {
- expect(findButton().attributes('aria-label')).toBe('Leave');
- });
-
- it('renders leave modal', () => {
- const leaveModal = wrapper.findComponent(LeaveModal);
-
- expect(leaveModal.exists()).toBe(true);
- expect(leaveModal.props('member')).toEqual(member);
- });
-
- it('triggers leave modal', () => {
- const binding = getBinding(findButton().element, 'gl-modal');
-
- expect(binding).not.toBeUndefined();
- expect(binding.value).toBe(LEAVE_MODAL_ID);
- });
-});
diff --git a/spec/frontend/members/components/action_buttons/remove_member_button_spec.js b/spec/frontend/members/components/action_buttons/remove_member_button_spec.js
index 0e5b667eb9b..cca340169b7 100644
--- a/spec/frontend/members/components/action_buttons/remove_member_button_spec.js
+++ b/spec/frontend/members/components/action_buttons/remove_member_button_spec.js
@@ -39,7 +39,6 @@ describe('RemoveMemberButton', () => {
},
propsData: {
memberId: 1,
- memberType: 'GroupMember',
message: 'Are you sure you want to remove John Smith?',
title: 'Remove member',
isAccessRequest: true,
@@ -77,20 +76,9 @@ describe('RemoveMemberButton', () => {
it('calls Vuex action to show `remove member` modal when clicked', () => {
findButton().vm.$emit('click');
- expect(actions.showRemoveMemberModal).toHaveBeenCalledWith(expect.any(Object), modalData);
- });
-
- describe('button optional properties', () => {
- it('has default value for category and text', () => {
- createComponent();
- expect(findButton().props('category')).toBe('secondary');
- expect(findButton().text()).toBe('');
- });
-
- it('allow changing value of button category and text', () => {
- createComponent({ buttonCategory: 'primary', buttonText: 'Decline request' });
- expect(findButton().props('category')).toBe('primary');
- expect(findButton().text()).toBe('Decline request');
+ expect(actions.showRemoveMemberModal).toHaveBeenCalledWith(expect.any(Object), {
+ ...modalData,
+ memberModelType: undefined,
});
});
});
diff --git a/spec/frontend/members/components/action_buttons/user_action_buttons_spec.js b/spec/frontend/members/components/action_buttons/user_action_buttons_spec.js
deleted file mode 100644
index 6ac46619bc9..00000000000
--- a/spec/frontend/members/components/action_buttons/user_action_buttons_spec.js
+++ /dev/null
@@ -1,161 +0,0 @@
-import { shallowMount } from '@vue/test-utils';
-import LeaveButton from '~/members/components/action_buttons/leave_button.vue';
-import RemoveMemberButton from '~/members/components/action_buttons/remove_member_button.vue';
-import UserActionButtons from '~/members/components/action_buttons/user_action_buttons.vue';
-import { parseUserDeletionObstacles } from '~/vue_shared/components/user_deletion_obstacles/utils';
-import { member, orphanedMember } from '../../mock_data';
-
-describe('UserActionButtons', () => {
- let wrapper;
-
- const createComponent = (propsData = {}) => {
- wrapper = shallowMount(UserActionButtons, {
- propsData: {
- member,
- isCurrentUser: false,
- isInvitedUser: false,
- ...propsData,
- },
- });
- };
-
- const findRemoveMemberButton = () => wrapper.findComponent(RemoveMemberButton);
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- describe('when user has `canRemove` permissions', () => {
- beforeEach(() => {
- createComponent({
- permissions: {
- canRemove: true,
- },
- });
- });
-
- it('renders remove member button', () => {
- expect(findRemoveMemberButton().exists()).toBe(true);
- });
-
- it('sets props correctly', () => {
- expect(findRemoveMemberButton().props()).toEqual({
- memberId: member.id,
- memberType: 'GroupMember',
- message: `Are you sure you want to remove ${member.user.name} from "${member.source.fullName}"?`,
- title: null,
- isAccessRequest: false,
- isInvite: false,
- icon: '',
- buttonCategory: 'secondary',
- buttonText: 'Remove member',
- userDeletionObstacles: {
- name: member.user.name,
- obstacles: parseUserDeletionObstacles(member.user),
- },
- });
- });
-
- describe('when member is orphaned', () => {
- it('sets `message` prop correctly', () => {
- createComponent({
- member: orphanedMember,
- permissions: {
- canRemove: true,
- },
- });
-
- expect(findRemoveMemberButton().props('message')).toBe(
- `Are you sure you want to remove this orphaned member from "${orphanedMember.source.fullName}"?`,
- );
- });
- });
-
- describe('when member is the current user', () => {
- it('renders leave button', () => {
- createComponent({
- isCurrentUser: true,
- permissions: {
- canRemove: true,
- },
- });
-
- expect(wrapper.findComponent(LeaveButton).exists()).toBe(true);
- });
- });
- });
-
- describe('when user does not have `canRemove` permissions', () => {
- it('does not render remove member button', () => {
- createComponent({
- permissions: {
- canRemove: false,
- },
- });
-
- expect(findRemoveMemberButton().exists()).toBe(false);
- });
- });
-
- describe('when group member', () => {
- beforeEach(() => {
- createComponent({
- member: {
- ...member,
- type: 'GroupMember',
- },
- permissions: {
- canRemove: true,
- },
- });
- });
-
- it('sets member type correctly', () => {
- expect(findRemoveMemberButton().props().memberType).toBe('GroupMember');
- });
- });
-
- describe('when project member', () => {
- beforeEach(() => {
- createComponent({
- member: {
- ...member,
- type: 'ProjectMember',
- },
- permissions: {
- canRemove: true,
- },
- });
- });
-
- it('sets member type correctly', () => {
- expect(findRemoveMemberButton().props().memberType).toBe('ProjectMember');
- });
- });
-
- describe('isInvitedUser', () => {
- it.each`
- isInvitedUser | icon | buttonText | buttonCategory
- ${true} | ${'remove'} | ${null} | ${'primary'}
- ${false} | ${''} | ${'Remove member'} | ${'secondary'}
- `(
- 'passes the correct props to remove-member-button when isInvitedUser is $isInvitedUser',
- ({ isInvitedUser, icon, buttonText, buttonCategory }) => {
- createComponent({
- isInvitedUser,
- permissions: {
- canRemove: true,
- },
- });
-
- expect(findRemoveMemberButton().props()).toEqual(
- expect.objectContaining({
- icon,
- buttonText,
- buttonCategory,
- }),
- );
- },
- );
- });
-});
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
new file mode 100644
index 00000000000..90f5b217007
--- /dev/null
+++ b/spec/frontend/members/components/action_dropdowns/leave_group_dropdown_item_spec.js
@@ -0,0 +1,54 @@
+import { GlDropdownItem } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+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, permissions } from '../../mock_data';
+
+describe('LeaveGroupDropdownItem', () => {
+ let wrapper;
+ const text = 'dummy';
+
+ const createComponent = (propsData = {}) => {
+ wrapper = shallowMount(LeaveGroupDropdownItem, {
+ propsData: {
+ member,
+ permissions,
+ ...propsData,
+ },
+ directives: {
+ GlModal: createMockDirective(),
+ },
+ slots: {
+ default: text,
+ },
+ });
+ };
+
+ const findDropdownItem = () => wrapper.findComponent(GlDropdownItem);
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('renders a slot with red text', () => {
+ expect(findDropdownItem().html()).toContain(`<span class="gl-text-red-500">${text}</span>`);
+ });
+
+ it('contains LeaveModal component', () => {
+ const leaveModal = wrapper.findComponent(LeaveModal);
+
+ expect(leaveModal.props()).toEqual({ member, permissions });
+ });
+
+ it('binds to the LeaveModal component', () => {
+ const binding = getBinding(findDropdownItem().element, 'gl-modal');
+
+ expect(binding.value).toBe(LEAVE_MODAL_ID);
+ });
+});
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
new file mode 100644
index 00000000000..e1c498249d7
--- /dev/null
+++ b/spec/frontend/members/components/action_dropdowns/remove_member_dropdown_item_spec.js
@@ -0,0 +1,77 @@
+import { GlDropdownItem } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import Vue from 'vue';
+import Vuex from 'vuex';
+import { modalData } from 'jest/members/mock_data';
+import RemoveMemberDropdownItem from '~/members/components/action_dropdowns/remove_member_dropdown_item.vue';
+import { MEMBER_TYPES, MEMBER_MODEL_TYPE_GROUP_MEMBER } from '~/members/constants';
+
+Vue.use(Vuex);
+
+describe('RemoveMemberDropdownItem', () => {
+ let wrapper;
+ const text = 'dummy';
+
+ const actions = {
+ showRemoveMemberModal: jest.fn(),
+ };
+
+ const createStore = (state = {}) => {
+ return new Vuex.Store({
+ modules: {
+ [MEMBER_TYPES.user]: {
+ namespaced: true,
+ state: {
+ memberPath: '/groups/foo-bar/-/group_members/:id',
+ ...state,
+ },
+ actions,
+ },
+ },
+ });
+ };
+
+ const createComponent = (propsData = {}, state) => {
+ wrapper = shallowMount(RemoveMemberDropdownItem, {
+ store: createStore(state),
+ provide: {
+ namespace: MEMBER_TYPES.user,
+ },
+ propsData: {
+ memberId: 1,
+ memberModelType: MEMBER_MODEL_TYPE_GROUP_MEMBER,
+ modalMessage: 'Are you sure you want to remove John Smith?',
+ isAccessRequest: true,
+ isInvite: true,
+ userDeletionObstacles: { name: 'user', obstacles: [] },
+ ...propsData,
+ },
+ slots: {
+ default: text,
+ },
+ });
+ };
+
+ const findDropdownItem = () => wrapper.findComponent(GlDropdownItem);
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('renders a slot with red text', () => {
+ expect(findDropdownItem().html()).toContain(`<span class="gl-text-red-500">${text}</span>`);
+ });
+
+ it('calls Vuex action to show `remove member` modal when clicked', () => {
+ findDropdownItem().vm.$emit('click');
+
+ 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
new file mode 100644
index 00000000000..5a2de1cac80
--- /dev/null
+++ b/spec/frontend/members/components/action_dropdowns/user_action_dropdown_spec.js
@@ -0,0 +1,220 @@
+import { shallowMount } from '@vue/test-utils';
+import { sprintf } from '~/locale';
+import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
+import LeaveGroupDropdownItem from '~/members/components/action_dropdowns/leave_group_dropdown_item.vue';
+import RemoveMemberDropdownItem from '~/members/components/action_dropdowns/remove_member_dropdown_item.vue';
+import UserActionDropdown from '~/members/components/action_dropdowns/user_action_dropdown.vue';
+import { I18N } from '~/members/components/action_dropdowns/constants';
+import {
+ MEMBER_MODEL_TYPE_GROUP_MEMBER,
+ MEMBER_MODEL_TYPE_PROJECT_MEMBER,
+} from '~/members/constants';
+import { parseUserDeletionObstacles } from '~/vue_shared/components/user_deletion_obstacles/utils';
+import { member, orphanedMember } from '../../mock_data';
+
+describe('UserActionDropdown', () => {
+ let wrapper;
+
+ const createComponent = (propsData = {}) => {
+ wrapper = shallowMount(UserActionDropdown, {
+ propsData: {
+ member,
+ isCurrentUser: false,
+ isInvitedUser: false,
+ ...propsData,
+ },
+ directives: {
+ GlTooltip: createMockDirective(),
+ },
+ });
+ };
+
+ const findRemoveMemberDropdownItem = () => wrapper.findComponent(RemoveMemberDropdownItem);
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('when user has `canRemove` permissions', () => {
+ beforeEach(() => {
+ createComponent({
+ permissions: {
+ canRemove: true,
+ },
+ });
+ });
+
+ it('renders remove member dropdown with correct text', () => {
+ const removeMemberDropdownItem = findRemoveMemberDropdownItem();
+ expect(removeMemberDropdownItem.exists()).toBe(true);
+ expect(removeMemberDropdownItem.html()).toContain(I18N.removeMember);
+ });
+
+ it('displays a tooltip', () => {
+ const tooltip = getBinding(wrapper.element, 'gl-tooltip');
+ expect(tooltip).not.toBeUndefined();
+ expect(tooltip.value).toBe(I18N.actions);
+ });
+
+ it('sets props correctly', () => {
+ expect(findRemoveMemberDropdownItem().props()).toEqual({
+ memberId: member.id,
+ memberModelType: MEMBER_MODEL_TYPE_GROUP_MEMBER,
+ modalMessage: sprintf(
+ I18N.confirmNormalUserRemoval,
+ {
+ userName: member.user.name,
+ group: member.source.fullName,
+ },
+ false,
+ ),
+ isAccessRequest: false,
+ isInvite: false,
+ userDeletionObstacles: {
+ name: member.user.name,
+ obstacles: parseUserDeletionObstacles(member.user),
+ },
+ preventRemoval: false,
+ });
+ });
+
+ describe('when member is orphaned', () => {
+ it('sets `message` prop correctly', () => {
+ createComponent({
+ member: orphanedMember,
+ permissions: {
+ canRemove: true,
+ },
+ });
+
+ expect(findRemoveMemberDropdownItem().props('modalMessage')).toBe(
+ sprintf(I18N.confirmOrphanedUserRemoval, { group: orphanedMember.source.fullName }),
+ );
+ });
+ });
+
+ describe('when member is the current user', () => {
+ it('renders leave dropdown with correct text', () => {
+ createComponent({
+ isCurrentUser: true,
+ permissions: {
+ canRemove: true,
+ },
+ });
+
+ const leaveGroupDropdownItem = wrapper.findComponent(LeaveGroupDropdownItem);
+ expect(leaveGroupDropdownItem.exists()).toBe(true);
+ expect(leaveGroupDropdownItem.html()).toContain(I18N.leaveGroup);
+ });
+ });
+ });
+
+ describe('when user does not have `canRemove` permissions', () => {
+ it('does not render remove member dropdown', () => {
+ createComponent({
+ permissions: {
+ canRemove: false,
+ },
+ });
+
+ expect(findRemoveMemberDropdownItem().exists()).toBe(false);
+ });
+ });
+
+ 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({
+ member: {
+ ...member,
+ type: MEMBER_MODEL_TYPE_GROUP_MEMBER,
+ },
+ permissions: {
+ canRemove: true,
+ },
+ });
+ });
+
+ it('sets member type correctly', () => {
+ expect(findRemoveMemberDropdownItem().props().memberModelType).toBe(
+ MEMBER_MODEL_TYPE_GROUP_MEMBER,
+ );
+ });
+ });
+
+ describe('when project member', () => {
+ beforeEach(() => {
+ createComponent({
+ member: {
+ ...member,
+ type: MEMBER_MODEL_TYPE_PROJECT_MEMBER,
+ },
+ permissions: {
+ canRemove: true,
+ },
+ });
+ });
+
+ it('sets member type correctly', () => {
+ expect(findRemoveMemberDropdownItem().props().memberModelType).toBe(
+ MEMBER_MODEL_TYPE_PROJECT_MEMBER,
+ );
+ });
+ });
+});
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 59b112492b8..47a03b5083a 100644
--- a/spec/frontend/members/components/modals/remove_member_modal_spec.js
+++ b/spec/frontend/members/components/modals/remove_member_modal_spec.js
@@ -3,7 +3,11 @@ import { shallowMount } from '@vue/test-utils';
import Vue from 'vue';
import Vuex from 'vuex';
import RemoveMemberModal from '~/members/components/modals/remove_member_modal.vue';
-import { MEMBER_TYPES } from '~/members/constants';
+import {
+ MEMBER_TYPES,
+ MEMBER_MODEL_TYPE_GROUP_MEMBER,
+ MEMBER_MODEL_TYPE_PROJECT_MEMBER,
+} from '~/members/constants';
import { OBSTACLE_TYPES } from '~/vue_shared/components/user_deletion_obstacles/constants';
import UserDeletionObstaclesList from '~/vue_shared/components/user_deletion_obstacles/user_deletion_obstacles_list.vue';
@@ -55,16 +59,16 @@ describe('RemoveMemberModal', () => {
});
describe.each`
- state | memberType | isAccessRequest | isInvite | actionText | removeSubMembershipsCheckboxExpected | unassignIssuablesCheckboxExpected | message | userDeletionObstacles | isPartOfOncall
- ${'removing a group member'} | ${'GroupMember'} | ${false} | ${false} | ${'Remove member'} | ${true} | ${true} | ${'Are you sure you want to remove Jane Doe from the Gitlab Org / Gitlab Test project?'} | ${{}} | ${false}
- ${'removing a project member'} | ${'ProjectMember'} | ${false} | ${false} | ${'Remove member'} | ${false} | ${true} | ${'Are you sure you want to remove Jane Doe from the Gitlab Org / Gitlab Test project?'} | ${mockObstacles} | ${true}
- ${'denying an access request'} | ${'ProjectMember'} | ${true} | ${false} | ${'Deny access request'} | ${false} | ${false} | ${"Are you sure you want to deny Jane Doe's request to join the Gitlab Org / Gitlab Test project?"} | ${{}} | ${false}
- ${'revoking invite'} | ${'ProjectMember'} | ${false} | ${true} | ${'Revoke invite'} | ${false} | ${false} | ${'Are you sure you want to revoke the invitation for foo@bar.com to join the Gitlab Org / Gitlab Test project?'} | ${mockObstacles} | ${false}
+ state | memberModelType | isAccessRequest | isInvite | actionText | removeSubMembershipsCheckboxExpected | unassignIssuablesCheckboxExpected | message | userDeletionObstacles | isPartOfOncall
+ ${'removing a group member'} | ${MEMBER_MODEL_TYPE_GROUP_MEMBER} | ${false} | ${false} | ${'Remove member'} | ${true} | ${true} | ${'Are you sure you want to remove Jane Doe from the Gitlab Org / Gitlab Test project?'} | ${{}} | ${false}
+ ${'removing a project member'} | ${MEMBER_MODEL_TYPE_PROJECT_MEMBER} | ${false} | ${false} | ${'Remove member'} | ${false} | ${true} | ${'Are you sure you want to remove Jane Doe from the Gitlab Org / Gitlab Test project?'} | ${mockObstacles} | ${true}
+ ${'denying an access request'} | ${MEMBER_MODEL_TYPE_PROJECT_MEMBER} | ${true} | ${false} | ${'Deny access request'} | ${false} | ${false} | ${"Are you sure you want to deny Jane Doe's request to join the Gitlab Org / Gitlab Test project?"} | ${{}} | ${false}
+ ${'revoking invite'} | ${MEMBER_MODEL_TYPE_PROJECT_MEMBER} | ${false} | ${true} | ${'Revoke invite'} | ${false} | ${false} | ${'Are you sure you want to revoke the invitation for foo@bar.com to join the Gitlab Org / Gitlab Test project?'} | ${mockObstacles} | ${false}
`(
'when $state',
({
actionText,
- memberType,
+ memberModelType,
isAccessRequest,
isInvite,
message,
@@ -79,7 +83,7 @@ describe('RemoveMemberModal', () => {
isInvite,
message,
memberPath,
- memberType,
+ memberModelType,
userDeletionObstacles,
});
});
@@ -133,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/__snapshots__/member_activity_spec.js.snap b/spec/frontend/members/components/table/__snapshots__/member_activity_spec.js.snap
new file mode 100644
index 00000000000..a0d9bae8a0b
--- /dev/null
+++ b/spec/frontend/members/components/table/__snapshots__/member_activity_spec.js.snap
@@ -0,0 +1,61 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`MemberActivity with a member that does not have all of the fields renders \`User created\` field 1`] = `
+<div>
+ <!---->
+
+ <div>
+ <strong>
+ Access granted:
+ </strong>
+
+ <span>
+
+ Aug 06, 2020
+
+ </span>
+ </div>
+
+ <!---->
+</div>
+`;
+
+exports[`MemberActivity with a member that has all fields renders \`User created\`, \`Access granted\`, and \`Last activity\` fields 1`] = `
+<div>
+ <div>
+ <strong>
+ User created:
+ </strong>
+
+ <span>
+
+ Mar 10, 2022
+
+ </span>
+ </div>
+
+ <div>
+ <strong>
+ Access granted:
+ </strong>
+
+ <span>
+
+ Jul 17, 2020
+
+ </span>
+ </div>
+
+ <div>
+ <strong>
+ Last activity:
+ </strong>
+
+ <span>
+
+ Mar 15, 2022
+
+ </span>
+ </div>
+</div>
+`;
diff --git a/spec/frontend/members/components/table/created_at_spec.js b/spec/frontend/members/components/table/created_at_spec.js
index 793c122587d..fa31177564b 100644
--- a/spec/frontend/members/components/table/created_at_spec.js
+++ b/spec/frontend/members/components/table/created_at_spec.js
@@ -1,20 +1,18 @@
-import { within } from '@testing-library/dom';
-import { mount, createWrapper } from '@vue/test-utils';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
import { useFakeDate } from 'helpers/fake_date';
import CreatedAt from '~/members/components/table/created_at.vue';
-import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
describe('CreatedAt', () => {
// March 15th, 2020
useFakeDate(2020, 2, 15);
const date = '2020-03-01T00:00:00.000';
- const dateTimeAgo = '2 weeks ago';
+ const formattedDate = 'Mar 01, 2020';
let wrapper;
const createComponent = (propsData) => {
- wrapper = mount(CreatedAt, {
+ wrapper = mountExtended(CreatedAt, {
propsData: {
date,
...propsData,
@@ -22,9 +20,6 @@ describe('CreatedAt', () => {
});
};
- const getByText = (text, options) =>
- createWrapper(within(wrapper.element).getByText(text, options));
-
afterEach(() => {
wrapper.destroy();
});
@@ -35,11 +30,7 @@ describe('CreatedAt', () => {
});
it('displays created at text', () => {
- expect(getByText(dateTimeAgo).exists()).toBe(true);
- });
-
- it('uses `TimeAgoTooltip` component to display tooltip', () => {
- expect(wrapper.findComponent(TimeAgoTooltip).exists()).toBe(true);
+ expect(wrapper.findByText(formattedDate).exists()).toBe(true);
});
});
@@ -52,7 +43,7 @@ describe('CreatedAt', () => {
},
});
- const link = getByText('Administrator');
+ const link = wrapper.findByRole('link', { name: 'Administrator' });
expect(link.exists()).toBe(true);
expect(link.attributes('href')).toBe('https://gitlab.com/root');
diff --git a/spec/frontend/members/components/table/member_action_buttons_spec.js b/spec/frontend/members/components/table/member_action_buttons_spec.js
index 03cfc6ca0f6..402a5e9db27 100644
--- a/spec/frontend/members/components/table/member_action_buttons_spec.js
+++ b/spec/frontend/members/components/table/member_action_buttons_spec.js
@@ -2,7 +2,7 @@ import { shallowMount } from '@vue/test-utils';
import AccessRequestActionButtons from '~/members/components/action_buttons/access_request_action_buttons.vue';
import GroupActionButtons from '~/members/components/action_buttons/group_action_buttons.vue';
import InviteActionButtons from '~/members/components/action_buttons/invite_action_buttons.vue';
-import UserActionButtons from '~/members/components/action_buttons/user_action_buttons.vue';
+import UserActionDropdown from '~/members/components/action_dropdowns/user_action_dropdown.vue';
import MemberActionButtons from '~/members/components/table/member_action_buttons.vue';
import { MEMBER_TYPES } from '~/members/constants';
import { member as memberMock, group, invite, accessRequest } from '../../mock_data';
@@ -29,7 +29,7 @@ describe('MemberActionButtons', () => {
it.each`
memberType | member | expectedComponent | expectedComponentName
- ${MEMBER_TYPES.user} | ${memberMock} | ${UserActionButtons} | ${'UserActionButtons'}
+ ${MEMBER_TYPES.user} | ${memberMock} | ${UserActionDropdown} | ${'UserActionDropdown'}
${MEMBER_TYPES.group} | ${group} | ${GroupActionButtons} | ${'GroupActionButtons'}
${MEMBER_TYPES.invite} | ${invite} | ${InviteActionButtons} | ${'InviteActionButtons'}
${MEMBER_TYPES.accessRequest} | ${accessRequest} | ${AccessRequestActionButtons} | ${'AccessRequestActionButtons'}
diff --git a/spec/frontend/members/components/table/member_activity_spec.js b/spec/frontend/members/components/table/member_activity_spec.js
new file mode 100644
index 00000000000..a372b40fd1f
--- /dev/null
+++ b/spec/frontend/members/components/table/member_activity_spec.js
@@ -0,0 +1,40 @@
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import MemberActivity from '~/members/components/table/member_activity.vue';
+import { member as memberMock, group as groupLinkMock } from '../../mock_data';
+
+describe('MemberActivity', () => {
+ let wrapper;
+
+ const defaultPropsData = {
+ member: memberMock,
+ };
+
+ const createComponent = ({ propsData = {} } = {}) => {
+ wrapper = mountExtended(MemberActivity, {
+ propsData: {
+ ...defaultPropsData,
+ ...propsData,
+ },
+ });
+ };
+
+ describe('with a member that has all fields', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('renders `User created`, `Access granted`, and `Last activity` fields', () => {
+ expect(wrapper.element).toMatchSnapshot();
+ });
+ });
+
+ describe('with a member that does not have all of the fields', () => {
+ beforeEach(() => {
+ createComponent({ propsData: { member: groupLinkMock } });
+ });
+
+ it('renders `User created` field', () => {
+ expect(wrapper.element).toMatchSnapshot();
+ });
+ });
+});
diff --git a/spec/frontend/members/components/table/member_source_spec.js b/spec/frontend/members/components/table/member_source_spec.js
index 2cd888207b1..fbfd0ca7ae7 100644
--- a/spec/frontend/members/components/table/member_source_spec.js
+++ b/spec/frontend/members/components/table/member_source_spec.js
@@ -1,19 +1,25 @@
-import { getByText as getByTextHelper } from '@testing-library/dom';
-import { mount, createWrapper } from '@vue/test-utils';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import MemberSource from '~/members/components/table/member_source.vue';
describe('MemberSource', () => {
let wrapper;
+ const memberSource = {
+ id: 102,
+ fullName: 'Foo bar',
+ webUrl: 'https://gitlab.com/groups/foo-bar',
+ };
+
+ const createdBy = {
+ name: 'Administrator',
+ webUrl: 'https://gitlab.com/root',
+ };
+
const createComponent = (propsData) => {
- wrapper = mount(MemberSource, {
+ wrapper = mountExtended(MemberSource, {
propsData: {
- memberSource: {
- id: 102,
- fullName: 'Foo bar',
- webUrl: 'https://gitlab.com/groups/foo-bar',
- },
+ memberSource,
...propsData,
},
directives: {
@@ -22,9 +28,6 @@ describe('MemberSource', () => {
});
};
- const getByText = (text, options) =>
- createWrapper(getByTextHelper(wrapper.element, text, options));
-
const getTooltipDirective = (elementWrapper) => getBinding(elementWrapper.element, 'gl-tooltip');
afterEach(() => {
@@ -32,40 +35,69 @@ describe('MemberSource', () => {
});
describe('direct member', () => {
- it('displays "Direct member"', () => {
- createComponent({
- isDirectMember: true,
+ describe('when created by is available', () => {
+ it('displays "Direct member by <user name>"', () => {
+ createComponent({
+ isDirectMember: true,
+ createdBy,
+ });
+
+ expect(wrapper.text()).toBe('Direct member by Administrator');
+ expect(wrapper.findByRole('link', { name: createdBy.name }).attributes('href')).toBe(
+ createdBy.webUrl,
+ );
});
+ });
- expect(getByText('Direct member').exists()).toBe(true);
+ describe('when created by is not available', () => {
+ it('displays "Direct member"', () => {
+ createComponent({
+ isDirectMember: true,
+ });
+
+ expect(wrapper.text()).toBe('Direct member');
+ });
});
});
describe('inherited member', () => {
- let sourceGroupLink;
-
- beforeEach(() => {
- createComponent({
- isDirectMember: false,
+ describe('when created by is available', () => {
+ beforeEach(() => {
+ createComponent({
+ isDirectMember: false,
+ createdBy,
+ });
});
- sourceGroupLink = getByText('Foo bar');
+ it('displays "<group name> by <user name>"', () => {
+ expect(wrapper.text()).toBe('Foo bar by Administrator');
+ expect(wrapper.findByRole('link', { name: memberSource.fullName }).attributes('href')).toBe(
+ memberSource.webUrl,
+ );
+ expect(wrapper.findByRole('link', { name: createdBy.name }).attributes('href')).toBe(
+ createdBy.webUrl,
+ );
+ });
});
- it('displays a link to source group', () => {
- createComponent({
- isDirectMember: false,
+ describe('when created by is not available', () => {
+ beforeEach(() => {
+ createComponent({
+ isDirectMember: false,
+ });
});
- expect(sourceGroupLink.exists()).toBe(true);
- expect(sourceGroupLink.attributes('href')).toBe('https://gitlab.com/groups/foo-bar');
- });
+ it('displays a link to source group', () => {
+ expect(wrapper.text()).toBe(memberSource.fullName);
+ expect(wrapper.attributes('href')).toBe(memberSource.webUrl);
+ });
- it('displays tooltip with "Inherited"', () => {
- const tooltipDirective = getTooltipDirective(sourceGroupLink);
+ it('displays tooltip with "Inherited"', () => {
+ const tooltipDirective = getTooltipDirective(wrapper);
- expect(tooltipDirective).not.toBeUndefined();
- expect(sourceGroupLink.attributes('title')).toBe('Inherited');
+ expect(tooltipDirective).not.toBeUndefined();
+ expect(tooltipDirective.value).toBe('Inherited');
+ });
});
});
});
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 0ed01396fcb..1d18026a410 100644
--- a/spec/frontend/members/components/table/members_table_spec.js
+++ b/spec/frontend/members/components/table/members_table_spec.js
@@ -8,9 +8,9 @@ import ExpirationDatepicker from '~/members/components/table/expiration_datepick
import MemberActionButtons from '~/members/components/table/member_action_buttons.vue';
import MemberAvatar from '~/members/components/table/member_avatar.vue';
import MemberSource from '~/members/components/table/member_source.vue';
+import MemberActivity from '~/members/components/table/member_activity.vue';
import MembersTable from '~/members/components/table/members_table.vue';
import RoleDropdown from '~/members/components/table/role_dropdown.vue';
-import UserDate from '~/vue_shared/components/user_date.vue';
import {
MEMBER_TYPES,
MEMBER_STATE_CREATED,
@@ -63,6 +63,7 @@ describe('MembersTable', () => {
provide: {
sourceId: 1,
currentUserId: 1,
+ canManageMembers: true,
namespace: MEMBER_TYPES.invite,
...provide,
},
@@ -106,16 +107,14 @@ describe('MembersTable', () => {
};
it.each`
- field | label | member | expectedComponent
- ${'account'} | ${'Account'} | ${memberMock} | ${MemberAvatar}
- ${'source'} | ${'Source'} | ${memberMock} | ${MemberSource}
- ${'granted'} | ${'Access granted'} | ${memberMock} | ${CreatedAt}
- ${'invited'} | ${'Invited'} | ${invite} | ${CreatedAt}
- ${'requested'} | ${'Requested'} | ${accessRequest} | ${CreatedAt}
- ${'maxRole'} | ${'Max role'} | ${memberCanUpdate} | ${RoleDropdown}
- ${'expiration'} | ${'Expiration'} | ${memberMock} | ${ExpirationDatepicker}
- ${'userCreatedAt'} | ${'Created on'} | ${memberMock} | ${UserDate}
- ${'lastActivityOn'} | ${'Last activity'} | ${memberMock} | ${UserDate}
+ field | label | member | expectedComponent
+ ${'account'} | ${'Account'} | ${memberMock} | ${MemberAvatar}
+ ${'source'} | ${'Source'} | ${memberMock} | ${MemberSource}
+ ${'invited'} | ${'Invited'} | ${invite} | ${CreatedAt}
+ ${'requested'} | ${'Requested'} | ${accessRequest} | ${CreatedAt}
+ ${'maxRole'} | ${'Max role'} | ${memberCanUpdate} | ${RoleDropdown}
+ ${'expiration'} | ${'Expiration'} | ${memberMock} | ${ExpirationDatepicker}
+ ${'activity'} | ${'Activity'} | ${memberMock} | ${MemberActivity}
`('renders the $label field', ({ field, label, member, expectedComponent }) => {
createComponent({
members: [member],
@@ -202,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'] });
@@ -230,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/components/table/role_dropdown_spec.js b/spec/frontend/members/components/table/role_dropdown_spec.js
index b254cce4d72..a11f67be8f5 100644
--- a/spec/frontend/members/components/table/role_dropdown_spec.js
+++ b/spec/frontend/members/components/table/role_dropdown_spec.js
@@ -4,11 +4,14 @@ import { within } from '@testing-library/dom';
import { mount, createWrapper } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
+import waitForPromises from 'helpers/wait_for_promises';
import RoleDropdown from '~/members/components/table/role_dropdown.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');
describe('RoleDropdown', () => {
let wrapper;
@@ -33,6 +36,10 @@ describe('RoleDropdown', () => {
wrapper = mount(RoleDropdown, {
provide: {
namespace: MEMBER_TYPES.user,
+ group: {
+ name: 'groupname',
+ path: '/grouppath/',
+ },
},
propsData: {
member,
@@ -63,12 +70,21 @@ describe('RoleDropdown', () => {
const findDropdownToggle = () => wrapper.find('button[aria-haspopup="true"]');
const findDropdown = () => wrapper.findComponent(GlDropdown);
+ let originalGon;
+
+ beforeEach(() => {
+ originalGon = window.gon;
+ gon.features = { showOverageOnRolePromotion: true };
+ });
+
afterEach(() => {
+ window.gon = originalGon;
wrapper.destroy();
});
describe('when dropdown is open', () => {
beforeEach(() => {
+ guestOverageConfirmAction.mockReturnValue(true);
createComponent();
return findDropdownToggle().trigger('click');
@@ -113,12 +129,16 @@ describe('RoleDropdown', () => {
expect($toast.show).toHaveBeenCalledWith('Role updated successfully.');
});
- it('disables dropdown while waiting for `updateMemberRole` to resolve', async () => {
+ it('puts dropdown in loading state while waiting for `updateMemberRole` to resolve', async () => {
await getDropdownItemByText('Developer').trigger('click');
- expect(findDropdown().props('disabled')).toBe(true);
+ expect(findDropdown().props('loading')).toBe(true);
+ });
+
+ it('enables dropdown after `updateMemberRole` resolves', async () => {
+ await getDropdownItemByText('Developer').trigger('click');
- await nextTick();
+ await waitForPromises();
expect(findDropdown().props('disabled')).toBe(false);
});
@@ -148,4 +168,44 @@ describe('RoleDropdown', () => {
expect(findDropdown().props('right')).toBe(false);
});
+
+ describe('guestOverageConfirmAction', () => {
+ const mockConfirmAction = ({ confirmed }) => {
+ guestOverageConfirmAction.mockResolvedValueOnce(confirmed);
+ };
+
+ beforeEach(() => {
+ createComponent();
+
+ findDropdownToggle().trigger('click');
+ });
+
+ afterEach(() => {
+ guestOverageConfirmAction.mockReset();
+ });
+
+ describe('when guestOverageConfirmAction returns true', () => {
+ beforeEach(() => {
+ mockConfirmAction({ confirmed: true });
+
+ getDropdownItemByText('Reporter').trigger('click');
+ });
+
+ it('calls updateMemberRole', () => {
+ expect(actions.updateMemberRole).toHaveBeenCalled();
+ });
+ });
+
+ describe('when guestOverageConfirmAction returns false', () => {
+ beforeEach(() => {
+ mockConfirmAction({ confirmed: false });
+
+ getDropdownItemByText('Reporter').trigger('click');
+ });
+
+ it('does not call updateMemberRole', () => {
+ expect(actions.updateMemberRole).not.toHaveBeenCalled();
+ });
+ });
+ });
});