diff options
Diffstat (limited to 'spec/frontend/vue_shared/components/members')
5 files changed, 351 insertions, 19 deletions
diff --git a/spec/frontend/vue_shared/components/members/mock_data.js b/spec/frontend/vue_shared/components/members/mock_data.js index d7bb8c0d142..5674929716d 100644 --- a/spec/frontend/vue_shared/components/members/mock_data.js +++ b/spec/frontend/vue_shared/components/members/mock_data.js @@ -3,6 +3,7 @@ export const member = { canUpdate: false, canRemove: false, canOverride: false, + isOverridden: false, accessLevel: { integerValue: 50, stringValue: 'Owner' }, source: { id: 178, diff --git a/spec/frontend/vue_shared/components/members/table/expiration_datepicker_spec.js b/spec/frontend/vue_shared/components/members/table/expiration_datepicker_spec.js new file mode 100644 index 00000000000..a1afdbc2b49 --- /dev/null +++ b/spec/frontend/vue_shared/components/members/table/expiration_datepicker_spec.js @@ -0,0 +1,166 @@ +import { mount, createLocalVue } from '@vue/test-utils'; +import Vuex from 'vuex'; +import { nextTick } from 'vue'; +import { GlDatepicker } from '@gitlab/ui'; +import { useFakeDate } from 'helpers/fake_date'; +import waitForPromises from 'helpers/wait_for_promises'; +import ExpirationDatepicker from '~/vue_shared/components/members/table/expiration_datepicker.vue'; +import { member } from '../mock_data'; + +const localVue = createLocalVue(); +localVue.use(Vuex); + +describe('ExpirationDatepicker', () => { + // March 15th, 2020 3:00 + useFakeDate(2020, 2, 15, 3); + + let wrapper; + let actions; + let resolveUpdateMemberExpiration; + const $toast = { + show: jest.fn(), + }; + + const createStore = () => { + actions = { + updateMemberExpiration: jest.fn( + () => + new Promise(resolve => { + resolveUpdateMemberExpiration = resolve; + }), + ), + }; + + return new Vuex.Store({ actions }); + }; + + const createComponent = (propsData = {}) => { + wrapper = mount(ExpirationDatepicker, { + propsData: { + member, + permissions: { canUpdate: true }, + ...propsData, + }, + localVue, + store: createStore(), + mocks: { + $toast, + }, + }); + }; + + const findInput = () => wrapper.find('input'); + const findDatepicker = () => wrapper.find(GlDatepicker); + + afterEach(() => { + wrapper.destroy(); + }); + + describe('datepicker input', () => { + it('sets `member.expiresAt` as initial date', async () => { + createComponent({ member: { ...member, expiresAt: '2020-03-17T00:00:00Z' } }); + + await nextTick(); + + expect(findInput().element.value).toBe('2020-03-17'); + }); + }); + + describe('props', () => { + beforeEach(() => { + createComponent(); + }); + + it('sets `minDate` prop as tomorrow', () => { + expect( + findDatepicker() + .props('minDate') + .toISOString(), + ).toBe(new Date('2020-3-16').toISOString()); + }); + + it('sets `target` prop as `null` so datepicker opens on focus', () => { + expect(findDatepicker().props('target')).toBe(null); + }); + + it("sets `container` prop as `null` so table styles don't affect the datepicker styles", () => { + expect(findDatepicker().props('container')).toBe(null); + }); + + it('shows clear button', () => { + expect(findDatepicker().props('showClearButton')).toBe(true); + }); + }); + + describe('when datepicker is changed', () => { + beforeEach(async () => { + createComponent(); + + findDatepicker().vm.$emit('input', new Date('2020-03-17')); + }); + + it('calls `updateMemberExpiration` Vuex action', () => { + expect(actions.updateMemberExpiration).toHaveBeenCalledWith(expect.any(Object), { + memberId: member.id, + expiresAt: new Date('2020-03-17'), + }); + }); + + it('displays toast when successful', async () => { + resolveUpdateMemberExpiration(); + await waitForPromises(); + + expect($toast.show).toHaveBeenCalledWith('Expiration date updated successfully.'); + }); + + it('disables dropdown while waiting for `updateMemberExpiration` to resolve', async () => { + expect(findDatepicker().props('disabled')).toBe(true); + + resolveUpdateMemberExpiration(); + await waitForPromises(); + + expect(findDatepicker().props('disabled')).toBe(false); + }); + }); + + describe('when datepicker is cleared', () => { + beforeEach(async () => { + createComponent(); + + findInput().setValue('2020-03-17'); + await nextTick(); + wrapper.find('[data-testid="clear-button"]').trigger('click'); + }); + + it('calls `updateMemberExpiration` Vuex action', () => { + expect(actions.updateMemberExpiration).toHaveBeenCalledWith(expect.any(Object), { + memberId: member.id, + expiresAt: null, + }); + }); + + it('displays toast when successful', async () => { + resolveUpdateMemberExpiration(); + await waitForPromises(); + + expect($toast.show).toHaveBeenCalledWith('Expiration date removed successfully.'); + }); + + it('disables datepicker while waiting for `updateMemberExpiration` to resolve', async () => { + expect(findDatepicker().props('disabled')).toBe(true); + + resolveUpdateMemberExpiration(); + await waitForPromises(); + + expect(findDatepicker().props('disabled')).toBe(false); + }); + }); + + describe('when user does not have `canUpdate` permissions', () => { + it('disables datepicker', () => { + createComponent({ permissions: { canUpdate: false } }); + + expect(findDatepicker().props('disabled')).toBe(true); + }); + }); +}); diff --git a/spec/frontend/vue_shared/components/members/table/members_table_spec.js b/spec/frontend/vue_shared/components/members/table/members_table_spec.js index 20c1c26d2ee..e593e88438c 100644 --- a/spec/frontend/vue_shared/components/members/table/members_table_spec.js +++ b/spec/frontend/vue_shared/components/members/table/members_table_spec.js @@ -3,14 +3,16 @@ import Vuex from 'vuex'; import { getByText as getByTextHelper, getByTestId as getByTestIdHelper, + within, } from '@testing-library/dom'; -import { GlBadge } from '@gitlab/ui'; +import { GlBadge, GlTable } from '@gitlab/ui'; import MembersTable from '~/vue_shared/components/members/table/members_table.vue'; import MemberAvatar from '~/vue_shared/components/members/table/member_avatar.vue'; import MemberSource from '~/vue_shared/components/members/table/member_source.vue'; import ExpiresAt from '~/vue_shared/components/members/table/expires_at.vue'; import CreatedAt from '~/vue_shared/components/members/table/created_at.vue'; import RoleDropdown from '~/vue_shared/components/members/table/role_dropdown.vue'; +import ExpirationDatepicker from '~/vue_shared/components/members/table/expiration_datepicker.vue'; import MemberActionButtons from '~/vue_shared/components/members/table/member_action_buttons.vue'; import * as initUserPopovers from '~/user_popovers'; import { member as memberMock, invite, accessRequest } from '../mock_data'; @@ -26,7 +28,12 @@ describe('MemberList', () => { state: { members: [], tableFields: [], + tableAttrs: { + table: { 'data-qa-selector': 'members_list' }, + tr: { 'data-qa-selector': 'member_row' }, + }, sourceId: 1, + currentUserId: 1, ...state, }, }); @@ -44,6 +51,7 @@ describe('MemberList', () => { 'member-action-buttons', 'role-dropdown', 'remove-group-link-modal', + 'expiration-datepicker', ], }); }; @@ -54,18 +62,24 @@ describe('MemberList', () => { const getByTestId = (id, options) => createWrapper(getByTestIdHelper(wrapper.element, id, options)); + const findTable = () => wrapper.find(GlTable); + afterEach(() => { wrapper.destroy(); wrapper = null; }); describe('fields', () => { - const memberCanUpdate = { + const directMember = { ...memberMock, - canUpdate: true, source: { ...memberMock.source, id: 1 }, }; + const memberCanUpdate = { + ...directMember, + canUpdate: true, + }; + it.each` field | label | member | expectedComponent ${'account'} | ${'Account'} | ${memberMock} | ${MemberAvatar} @@ -75,7 +89,7 @@ describe('MemberList', () => { ${'requested'} | ${'Requested'} | ${accessRequest} | ${CreatedAt} ${'expires'} | ${'Access expires'} | ${memberMock} | ${ExpiresAt} ${'maxRole'} | ${'Max role'} | ${memberCanUpdate} | ${RoleDropdown} - ${'expiration'} | ${'Expiration'} | ${memberMock} | ${null} + ${'expiration'} | ${'Expiration'} | ${memberMock} | ${ExpirationDatepicker} `('renders the $label field', ({ field, label, member, expectedComponent }) => { createComponent({ members: [member], @@ -94,19 +108,60 @@ describe('MemberList', () => { } }); - it('renders "Actions" field for screen readers', () => { - createComponent({ members: [memberMock], tableFields: ['actions'] }); + describe('"Actions" field', () => { + it('renders "Actions" field for screen readers', () => { + createComponent({ members: [memberCanUpdate], tableFields: ['actions'] }); - const actionField = getByTestId('col-actions'); + const actionField = getByTestId('col-actions'); - expect(actionField.exists()).toBe(true); - expect(actionField.classes('gl-sr-only')).toBe(true); - expect( - wrapper - .find(`[data-label="Actions"][role="cell"]`) - .find(MemberActionButtons) - .exists(), - ).toBe(true); + expect(actionField.exists()).toBe(true); + expect(actionField.classes('gl-sr-only')).toBe(true); + expect( + wrapper + .find(`[data-label="Actions"][role="cell"]`) + .find(MemberActionButtons) + .exists(), + ).toBe(true); + }); + + describe('when user is not logged in', () => { + it('does not render the "Actions" field', () => { + createComponent({ currentUserId: null, tableFields: ['actions'] }); + + expect(within(wrapper.element).queryByTestId('col-actions')).toBe(null); + }); + }); + + const memberCanRemove = { + ...directMember, + canRemove: true, + }; + + describe.each` + permission | members + ${'canUpdate'} | ${[memberCanUpdate]} + ${'canRemove'} | ${[memberCanRemove]} + ${'canResend'} | ${[invite]} + `('when one of the members has $permission permissions', ({ members }) => { + it('renders the "Actions" field', () => { + createComponent({ members, tableFields: ['actions'] }); + + expect(getByTestId('col-actions').exists()).toBe(true); + }); + }); + + describe.each` + permission | members + ${'canUpdate'} | ${[memberMock]} + ${'canRemove'} | ${[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'] }); + + expect(within(wrapper.element).queryByTestId('col-actions')).toBe(null); + }); + }); }); }); @@ -138,4 +193,20 @@ describe('MemberList', () => { expect(initUserPopoversMock).toHaveBeenCalled(); }); + + it('adds QA selector to table', () => { + createComponent(); + + expect(findTable().attributes('data-qa-selector')).toBe('members_list'); + }); + + it('adds QA selector to table row', () => { + createComponent(); + + expect( + findTable() + .find('tbody tr') + .attributes('data-qa-selector'), + ).toBe('member_row'); + }); }); diff --git a/spec/frontend/vue_shared/components/members/table/role_dropdown_spec.js b/spec/frontend/vue_shared/components/members/table/role_dropdown_spec.js index 1e47953a510..55ec7000693 100644 --- a/spec/frontend/vue_shared/components/members/table/role_dropdown_spec.js +++ b/spec/frontend/vue_shared/components/members/table/role_dropdown_spec.js @@ -30,6 +30,7 @@ describe('RoleDropdown', () => { wrapper = mount(RoleDropdown, { propsData: { member, + permissions: {}, ...propsData, }, localVue, @@ -115,11 +116,11 @@ describe('RoleDropdown', () => { await nextTick(); - expect(findDropdown().attributes('disabled')).toBe('disabled'); + expect(findDropdown().props('disabled')).toBe(true); await waitForPromises(); - expect(findDropdown().attributes('disabled')).toBeUndefined(); + expect(findDropdown().props('disabled')).toBe(false); }); }); }); diff --git a/spec/frontend/vue_shared/components/members/utils_spec.js b/spec/frontend/vue_shared/components/members/utils_spec.js index f183abc08d6..3f2b2097133 100644 --- a/spec/frontend/vue_shared/components/members/utils_spec.js +++ b/spec/frontend/vue_shared/components/members/utils_spec.js @@ -1,5 +1,19 @@ -import { generateBadges } from '~/vue_shared/components/members/utils'; -import { member as memberMock } from './mock_data'; +import { + generateBadges, + isGroup, + isDirectMember, + isCurrentUser, + canRemove, + canResend, + canUpdate, + canOverride, +} from '~/vue_shared/components/members/utils'; +import { member as memberMock, group, invite } from './mock_data'; + +const DIRECT_MEMBER_ID = 178; +const INHERITED_MEMBER_ID = 179; +const IS_CURRENT_USER_ID = 123; +const IS_NOT_CURRENT_USER_ID = 124; describe('Members Utils', () => { describe('generateBadges', () => { @@ -26,4 +40,83 @@ describe('Members Utils', () => { expect(generateBadges(member, true)).toContainEqual(expect.objectContaining(expected)); }); }); + + describe('isGroup', () => { + test.each` + member | expected + ${group} | ${true} + ${memberMock} | ${false} + `('returns $expected', ({ member, expected }) => { + expect(isGroup(member)).toBe(expected); + }); + }); + + describe('isDirectMember', () => { + test.each` + sourceId | expected + ${DIRECT_MEMBER_ID} | ${true} + ${INHERITED_MEMBER_ID} | ${false} + `('returns $expected', ({ sourceId, expected }) => { + expect(isDirectMember(memberMock, sourceId)).toBe(expected); + }); + }); + + describe('isCurrentUser', () => { + test.each` + currentUserId | expected + ${IS_CURRENT_USER_ID} | ${true} + ${IS_NOT_CURRENT_USER_ID} | ${false} + `('returns $expected', ({ currentUserId, expected }) => { + expect(isCurrentUser(memberMock, currentUserId)).toBe(expected); + }); + }); + + describe('canRemove', () => { + const memberCanRemove = { + ...memberMock, + canRemove: true, + }; + + test.each` + member | sourceId | expected + ${memberCanRemove} | ${DIRECT_MEMBER_ID} | ${true} + ${memberCanRemove} | ${INHERITED_MEMBER_ID} | ${false} + ${memberMock} | ${INHERITED_MEMBER_ID} | ${false} + `('returns $expected', ({ member, sourceId, expected }) => { + expect(canRemove(member, sourceId)).toBe(expected); + }); + }); + + describe('canResend', () => { + test.each` + member | expected + ${invite} | ${true} + ${{ ...invite, invite: { ...invite.invite, canResend: false } }} | ${false} + `('returns $expected', ({ member, sourceId, expected }) => { + expect(canResend(member, sourceId)).toBe(expected); + }); + }); + + describe('canUpdate', () => { + const memberCanUpdate = { + ...memberMock, + canUpdate: true, + }; + + test.each` + member | currentUserId | sourceId | expected + ${memberCanUpdate} | ${IS_NOT_CURRENT_USER_ID} | ${DIRECT_MEMBER_ID} | ${true} + ${memberCanUpdate} | ${IS_CURRENT_USER_ID} | ${DIRECT_MEMBER_ID} | ${false} + ${memberCanUpdate} | ${IS_CURRENT_USER_ID} | ${INHERITED_MEMBER_ID} | ${false} + ${memberMock} | ${IS_NOT_CURRENT_USER_ID} | ${DIRECT_MEMBER_ID} | ${false} + `('returns $expected', ({ member, currentUserId, sourceId, expected }) => { + expect(canUpdate(member, currentUserId, sourceId)).toBe(expected); + }); + }); + + describe('canOverride', () => { + it('returns `false`', () => { + expect(canOverride(memberMock)).toBe(false); + }); + }); }); |