From d9ab72d6080f594d0b3cae15f14b3ef2c6c638cb Mon Sep 17 00:00:00 2001 From: GitLab Bot Date: Wed, 20 Oct 2021 08:43:02 +0000 Subject: Add latest changes from gitlab-org/gitlab@14-4-stable-ee --- .../action_buttons/remove_member_button_spec.js | 2 +- .../action_buttons/user_action_buttons_spec.js | 5 +- .../members/components/modals/leave_modal_spec.js | 29 +++---- .../components/modals/remove_member_modal_spec.js | 33 ++++---- .../members/components/table/expires_at_spec.js | 86 --------------------- .../members/components/table/members_table_spec.js | 88 +++++++++++++++------- spec/frontend/members/mock_data.js | 7 +- 7 files changed, 103 insertions(+), 147 deletions(-) delete mode 100644 spec/frontend/members/components/table/expires_at_spec.js (limited to 'spec/frontend/members') 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 d8453d453e7..7eb0ea37fe6 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 @@ -45,7 +45,7 @@ describe('RemoveMemberButton', () => { title: 'Remove member', isAccessRequest: true, isInvite: true, - oncallSchedules: { name: 'user', schedules: [] }, + userDeletionObstacles: { name: 'user', obstacles: [] }, ...propsData, }, directives: { 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 index 0aa3780f030..10e451376c8 100644 --- a/spec/frontend/members/components/action_buttons/user_action_buttons_spec.js +++ b/spec/frontend/members/components/action_buttons/user_action_buttons_spec.js @@ -2,6 +2,7 @@ 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', () => { @@ -45,9 +46,9 @@ describe('UserActionButtons', () => { isAccessRequest: false, isInvite: false, icon: 'remove', - oncallSchedules: { + userDeletionObstacles: { name: member.user.name, - schedules: member.user.oncallSchedules, + obstacles: parseUserDeletionObstacles(member.user), }, }); }); diff --git a/spec/frontend/members/components/modals/leave_modal_spec.js b/spec/frontend/members/components/modals/leave_modal_spec.js index 1dc913e5c78..f755f08dbf2 100644 --- a/spec/frontend/members/components/modals/leave_modal_spec.js +++ b/spec/frontend/members/components/modals/leave_modal_spec.js @@ -6,7 +6,8 @@ import { nextTick } from 'vue'; import Vuex from 'vuex'; import LeaveModal from '~/members/components/modals/leave_modal.vue'; import { LEAVE_MODAL_ID, MEMBER_TYPES } from '~/members/constants'; -import OncallSchedulesList from '~/vue_shared/components/oncall_schedules_list.vue'; +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'; jest.mock('~/lib/utils/csrf', () => ({ token: 'mock-csrf-token' })); @@ -51,7 +52,7 @@ describe('LeaveModal', () => { const findModal = () => wrapper.findComponent(GlModal); const findForm = () => findModal().findComponent(GlForm); - const findOncallSchedulesList = () => findModal().findComponent(OncallSchedulesList); + const findUserDeletionObstaclesList = () => findModal().findComponent(UserDeletionObstaclesList); const getByText = (text, options) => createWrapper(within(findModal().element).getByText(text, options)); @@ -89,25 +90,27 @@ describe('LeaveModal', () => { ); }); - describe('On-call schedules list', () => { - it("displays oncall schedules list when member's user is part of on-call schedules ", () => { - const schedulesList = findOncallSchedulesList(); - expect(schedulesList.exists()).toBe(true); - expect(schedulesList.props()).toMatchObject({ + describe('User deletion obstacles list', () => { + it("displays obstacles list when member's user is part of on-call management", () => { + const obstaclesList = findUserDeletionObstaclesList(); + expect(obstaclesList.exists()).toBe(true); + expect(obstaclesList.props()).toMatchObject({ isCurrentUser: true, - schedules: member.user.oncallSchedules, + obstacles: parseUserDeletionObstacles(member.user), }); }); - it("does NOT display oncall schedules list when member's user is NOT a part of on-call schedules ", async () => { + it("does NOT display obstacles list when member's user is NOT a part of on-call management", async () => { wrapper.destroy(); - const memberWithoutOncallSchedules = cloneDeep(member); - delete memberWithoutOncallSchedules.user.oncallSchedules; - createComponent({ member: memberWithoutOncallSchedules }); + const memberWithoutOncall = cloneDeep(member); + delete memberWithoutOncall.user.oncallSchedules; + delete memberWithoutOncall.user.escalationPolicies; + + createComponent({ member: memberWithoutOncall }); await nextTick(); - expect(findOncallSchedulesList().exists()).toBe(false); + expect(findUserDeletionObstaclesList().exists()).toBe(false); }); }); 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 1dc41582c12..1d39c4b3175 100644 --- a/spec/frontend/members/components/modals/remove_member_modal_spec.js +++ b/spec/frontend/members/components/modals/remove_member_modal_spec.js @@ -4,15 +4,19 @@ 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 OncallSchedulesList from '~/vue_shared/components/oncall_schedules_list.vue'; +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'; Vue.use(Vuex); describe('RemoveMemberModal', () => { const memberPath = '/gitlab-org/gitlab-test/-/project_members/90'; - const mockSchedules = { + const mockObstacles = { name: 'User1', - schedules: [{ id: 1, name: 'Schedule 1' }], + obstacles: [ + { name: 'Schedule 1', type: OBSTACLE_TYPES.oncallSchedules }, + { name: 'Policy 1', type: OBSTACLE_TYPES.escalationPolicies }, + ], }; let wrapper; @@ -44,18 +48,18 @@ describe('RemoveMemberModal', () => { const findForm = () => wrapper.find({ ref: 'form' }); const findGlModal = () => wrapper.findComponent(GlModal); - const findOnCallSchedulesList = () => wrapper.findComponent(OncallSchedulesList); + const findUserDeletionObstaclesList = () => wrapper.findComponent(UserDeletionObstaclesList); afterEach(() => { wrapper.destroy(); }); describe.each` - state | memberType | isAccessRequest | isInvite | actionText | removeSubMembershipsCheckboxExpected | unassignIssuablesCheckboxExpected | message | onCallSchedules - ${'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?'} | ${{}} - ${'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?'} | ${mockSchedules} - ${'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?"} | ${{}} - ${'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?'} | ${mockSchedules} + 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} `( 'when $state', ({ @@ -66,7 +70,8 @@ describe('RemoveMemberModal', () => { message, removeSubMembershipsCheckboxExpected, unassignIssuablesCheckboxExpected, - onCallSchedules, + userDeletionObstacles, + isPartOfOncall, }) => { beforeEach(() => { createComponent({ @@ -75,12 +80,10 @@ describe('RemoveMemberModal', () => { message, memberPath, memberType, - onCallSchedules, + userDeletionObstacles, }); }); - const isPartOfOncallSchedules = Boolean(isAccessRequest && onCallSchedules.schedules?.length); - it(`has the title ${actionText}`, () => { expect(findGlModal().attributes('title')).toBe(actionText); }); @@ -109,8 +112,8 @@ describe('RemoveMemberModal', () => { ); }); - it(`shows ${isPartOfOncallSchedules ? 'all' : 'no'} related on-call schedules`, () => { - expect(findOnCallSchedulesList().exists()).toBe(isPartOfOncallSchedules); + it(`shows ${isPartOfOncall ? 'all' : 'no'} related on-call schedules or policies`, () => { + expect(findUserDeletionObstaclesList().exists()).toBe(isPartOfOncall); }); it('submits the form when the modal is submitted', () => { diff --git a/spec/frontend/members/components/table/expires_at_spec.js b/spec/frontend/members/components/table/expires_at_spec.js deleted file mode 100644 index 2b8e6ab8f2a..00000000000 --- a/spec/frontend/members/components/table/expires_at_spec.js +++ /dev/null @@ -1,86 +0,0 @@ -import { within } from '@testing-library/dom'; -import { mount, createWrapper } from '@vue/test-utils'; -import { useFakeDate } from 'helpers/fake_date'; -import { createMockDirective, getBinding } from 'helpers/vue_mock_directive'; -import ExpiresAt from '~/members/components/table/expires_at.vue'; - -describe('ExpiresAt', () => { - // March 15th, 2020 - useFakeDate(2020, 2, 15); - - let wrapper; - - const createComponent = (propsData) => { - wrapper = mount(ExpiresAt, { - propsData, - directives: { - GlTooltip: createMockDirective(), - }, - }); - }; - - const getByText = (text, options) => - createWrapper(within(wrapper.element).getByText(text, options)); - - const getTooltipDirective = (elementWrapper) => getBinding(elementWrapper.element, 'gl-tooltip'); - - afterEach(() => { - wrapper.destroy(); - }); - - describe('when no expiration date is set', () => { - it('displays "No expiration set"', () => { - createComponent({ date: null }); - - expect(getByText('No expiration set').exists()).toBe(true); - }); - }); - - describe('when expiration date is in the past', () => { - let expiredText; - - beforeEach(() => { - createComponent({ date: '2019-03-15T00:00:00.000' }); - - expiredText = getByText('Expired'); - }); - - it('displays "Expired"', () => { - expect(expiredText.exists()).toBe(true); - expect(expiredText.classes()).toContain('gl-text-red-500'); - }); - - it('displays tooltip with formatted date', () => { - const tooltipDirective = getTooltipDirective(expiredText); - - expect(tooltipDirective).not.toBeUndefined(); - expect(expiredText.attributes('title')).toBe('Mar 15, 2019 12:00am UTC'); - }); - }); - - describe('when expiration date is in the future', () => { - it.each` - date | expected | warningColor - ${'2020-03-23T00:00:00.000'} | ${'in 8 days'} | ${false} - ${'2020-03-20T00:00:00.000'} | ${'in 5 days'} | ${true} - ${'2020-03-16T00:00:00.000'} | ${'in 1 day'} | ${true} - ${'2020-03-15T05:00:00.000'} | ${'in about 5 hours'} | ${true} - ${'2020-03-15T01:00:00.000'} | ${'in about 1 hour'} | ${true} - ${'2020-03-15T00:30:00.000'} | ${'in 30 minutes'} | ${true} - ${'2020-03-15T00:01:15.000'} | ${'in 1 minute'} | ${true} - ${'2020-03-15T00:00:15.000'} | ${'in less than a minute'} | ${true} - `('displays "$expected"', ({ date, expected, warningColor }) => { - createComponent({ date }); - - const expiredText = getByText(expected); - - expect(expiredText.exists()).toBe(true); - - if (warningColor) { - expect(expiredText.classes()).toContain('gl-text-orange-500'); - } else { - expect(expiredText.classes()).not.toContain('gl-text-orange-500'); - } - }); - }); -}); diff --git a/spec/frontend/members/components/table/members_table_spec.js b/spec/frontend/members/components/table/members_table_spec.js index 6885da53b26..580e5edd652 100644 --- a/spec/frontend/members/components/table/members_table_spec.js +++ b/spec/frontend/members/components/table/members_table_spec.js @@ -1,22 +1,24 @@ import { GlBadge, GlPagination, GlTable } from '@gitlab/ui'; -import { - getByText as getByTextHelper, - getByTestId as getByTestIdHelper, - within, -} from '@testing-library/dom'; -import { mount, createLocalVue, createWrapper } from '@vue/test-utils'; +import { createLocalVue } from '@vue/test-utils'; import Vuex from 'vuex'; import setWindowLocation from 'helpers/set_window_location_helper'; -import { extendedWrapper } from 'helpers/vue_test_utils_helper'; +import { mountExtended, extendedWrapper } from 'helpers/vue_test_utils_helper'; import CreatedAt from '~/members/components/table/created_at.vue'; import ExpirationDatepicker from '~/members/components/table/expiration_datepicker.vue'; -import ExpiresAt from '~/members/components/table/expires_at.vue'; 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 MembersTable from '~/members/components/table/members_table.vue'; import RoleDropdown from '~/members/components/table/role_dropdown.vue'; -import { MEMBER_TYPES, TAB_QUERY_PARAM_VALUES } from '~/members/constants'; +import { + MEMBER_TYPES, + MEMBER_STATE_CREATED, + MEMBER_STATE_AWAITING, + MEMBER_STATE_ACTIVE, + USER_STATE_BLOCKED_PENDING_APPROVAL, + BADGE_LABELS_PENDING_OWNER_APPROVAL, + TAB_QUERY_PARAM_VALUES, +} from '~/members/constants'; import * as initUserPopovers from '~/user_popovers'; import { member as memberMock, @@ -53,7 +55,7 @@ describe('MembersTable', () => { }; const createComponent = (state, provide = {}) => { - wrapper = mount(MembersTable, { + wrapper = mountExtended(MembersTable, { localVue, propsData: { tabQueryParamValue: TAB_QUERY_PARAM_VALUES.invite, @@ -68,7 +70,6 @@ describe('MembersTable', () => { stubs: [ 'member-avatar', 'member-source', - 'expires-at', 'created-at', 'member-action-buttons', 'role-dropdown', @@ -81,17 +82,11 @@ describe('MembersTable', () => { const url = 'https://localhost/foo-bar/-/project_members?tab=invited'; - const getByText = (text, options) => - createWrapper(getByTextHelper(wrapper.element, text, options)); - - const getByTestId = (id, options) => - createWrapper(getByTestIdHelper(wrapper.element, id, options)); - const findTable = () => wrapper.find(GlTable); const findTableCellByMemberId = (tableCellLabel, memberId) => - getByTestId(`members-table-row-${memberId}`).find( - `[data-label="${tableCellLabel}"][role="cell"]`, - ); + wrapper + .findByTestId(`members-table-row-${memberId}`) + .find(`[data-label="${tableCellLabel}"][role="cell"]`); const findPagination = () => extendedWrapper(wrapper.find(GlPagination)); @@ -103,7 +98,6 @@ describe('MembersTable', () => { afterEach(() => { wrapper.destroy(); - wrapper = null; }); describe('fields', () => { @@ -119,7 +113,6 @@ describe('MembersTable', () => { ${'granted'} | ${'Access granted'} | ${memberMock} | ${CreatedAt} ${'invited'} | ${'Invited'} | ${invite} | ${CreatedAt} ${'requested'} | ${'Requested'} | ${accessRequest} | ${CreatedAt} - ${'expires'} | ${'Access expires'} | ${memberMock} | ${ExpiresAt} ${'maxRole'} | ${'Max role'} | ${memberCanUpdate} | ${RoleDropdown} ${'expiration'} | ${'Expiration'} | ${memberMock} | ${ExpirationDatepicker} `('renders the $label field', ({ field, label, member, expectedComponent }) => { @@ -128,7 +121,7 @@ describe('MembersTable', () => { tableFields: [field], }); - expect(getByText(label, { selector: '[role="columnheader"]' }).exists()).toBe(true); + expect(wrapper.findByText(label, { selector: '[role="columnheader"]' }).exists()).toBe(true); if (expectedComponent) { expect( @@ -137,11 +130,50 @@ describe('MembersTable', () => { } }); + describe('Invited column', () => { + describe.each` + state | userState | expectedBadgeLabel + ${MEMBER_STATE_CREATED} | ${null} | ${''} + ${MEMBER_STATE_CREATED} | ${USER_STATE_BLOCKED_PENDING_APPROVAL} | ${BADGE_LABELS_PENDING_OWNER_APPROVAL} + ${MEMBER_STATE_AWAITING} | ${''} | ${''} + ${MEMBER_STATE_AWAITING} | ${USER_STATE_BLOCKED_PENDING_APPROVAL} | ${BADGE_LABELS_PENDING_OWNER_APPROVAL} + ${MEMBER_STATE_AWAITING} | ${'something_else'} | ${BADGE_LABELS_PENDING_OWNER_APPROVAL} + ${MEMBER_STATE_ACTIVE} | ${null} | ${''} + ${MEMBER_STATE_ACTIVE} | ${'something_else'} | ${''} + `('Invited Badge', ({ state, userState, expectedBadgeLabel }) => { + it(`${ + expectedBadgeLabel ? 'shows' : 'hides' + } invited badge if user status: '${userState}' and member state: '${state}'`, () => { + createComponent({ + members: [ + { + ...invite, + state, + invite: { + ...invite.invite, + userState, + }, + }, + ], + tableFields: ['invited'], + }); + + const invitedTab = wrapper.findByTestId('invited-badge'); + + if (expectedBadgeLabel) { + expect(invitedTab.text()).toBe(expectedBadgeLabel); + } else { + expect(invitedTab.exists()).toBe(false); + } + }); + }); + }); + describe('"Actions" field', () => { it('renders "Actions" field for screen readers', () => { createComponent({ members: [memberCanUpdate], tableFields: ['actions'] }); - const actionField = getByTestId('col-actions'); + const actionField = wrapper.findByTestId('col-actions'); expect(actionField.exists()).toBe(true); expect(actionField.classes('gl-sr-only')).toBe(true); @@ -154,7 +186,7 @@ describe('MembersTable', () => { it('does not render the "Actions" field', () => { createComponent({ tableFields: ['actions'] }, { currentUserId: null }); - expect(within(wrapper.element).queryByTestId('col-actions')).toBe(null); + expect(wrapper.findByTestId('col-actions').exists()).toBe(false); }); }); @@ -177,7 +209,7 @@ describe('MembersTable', () => { it('renders the "Actions" field', () => { createComponent({ members, tableFields: ['actions'] }); - expect(getByTestId('col-actions').exists()).toBe(true); + expect(wrapper.findByTestId('col-actions').exists()).toBe(true); expect(findTableCellByMemberId('Actions', members[0].id).classes()).toStrictEqual([ 'col-actions', @@ -199,7 +231,7 @@ describe('MembersTable', () => { it('does not render the "Actions" field', () => { createComponent({ members, tableFields: ['actions'] }); - expect(within(wrapper.element).queryByTestId('col-actions')).toBe(null); + expect(wrapper.findByTestId('col-actions').exists()).toBe(false); }); }); }); @@ -209,7 +241,7 @@ describe('MembersTable', () => { it('displays a "No members found" message', () => { createComponent(); - expect(getByText('No members found').exists()).toBe(true); + expect(wrapper.findByText('No members found').exists()).toBe(true); }); }); diff --git a/spec/frontend/members/mock_data.js b/spec/frontend/members/mock_data.js index eb9f905fea2..f42ee295511 100644 --- a/spec/frontend/members/mock_data.js +++ b/spec/frontend/members/mock_data.js @@ -1,4 +1,4 @@ -import { MEMBER_TYPES } from '~/members/constants'; +import { MEMBER_TYPES, MEMBER_STATE_CREATED } from '~/members/constants'; export const member = { requestedAt: null, @@ -14,6 +14,7 @@ export const member = { webUrl: 'https://gitlab.com/groups/foo-bar', }, type: 'GroupMember', + state: MEMBER_STATE_CREATED, user: { id: 123, name: 'Administrator', @@ -23,6 +24,7 @@ export const member = { blocked: false, twoFactorEnabled: false, oncallSchedules: [{ name: 'schedule 1' }], + escalationPolicies: [{ name: 'policy 1' }], }, id: 238, createdAt: '2020-07-17T16:22:46.923Z', @@ -63,12 +65,13 @@ export const modalData = { memberPath: '/groups/foo-bar/-/group_members/1', memberType: 'GroupMember', message: 'Are you sure you want to remove John Smith?', - oncallSchedules: { name: 'user', schedules: [] }, + userDeletionObstacles: { name: 'user', obstacles: [] }, }; const { user, ...memberNoUser } = member; export const invite = { ...memberNoUser, + state: MEMBER_STATE_CREATED, invite: { email: 'jewel@hudsonwalter.biz', avatarUrl: 'https://www.gravatar.com/avatar/cbab7510da7eec2f60f638261b05436d?s=80&d=identicon', -- cgit v1.2.1