diff --git a/ee/app/assets/javascripts/usage_quotas/seats/components/subscription_seat_details.vue b/ee/app/assets/javascripts/usage_quotas/seats/components/subscription_seat_details.vue index dc5e53e688f5c536d5dbb338ea1f73846615c017..ed3bc028afccc066ed89d18fc14940c3801ad350 100644 --- a/ee/app/assets/javascripts/usage_quotas/seats/components/subscription_seat_details.vue +++ b/ee/app/assets/javascripts/usage_quotas/seats/components/subscription_seat_details.vue @@ -1,6 +1,6 @@ <script> import { GlTable, GlBadge, GlLink } from '@gitlab/ui'; -import { mapActions, mapGetters } from 'vuex'; +import { mapState, mapActions } from 'vuex'; import { formatDate } from '~/lib/utils/datetime_utility'; import { DETAILS_FIELDS } from '../constants'; import SubscriptionSeatDetailsLoader from './subscription_seat_details_loader.vue'; @@ -20,15 +20,16 @@ export default { }, }, computed: { - ...mapGetters(['membershipsById']), - state() { - return this.membershipsById(this.seatMemberId); - }, + ...mapState({ + userDetailsEntry(state) { + return state.userDetails[this.seatMemberId]; + }, + }), items() { - return this.state.items; + return this.userDetailsEntry.items; }, - isLoading() { - return this.state.isLoading; + isLoaderShown() { + return this.userDetailsEntry.isLoading || this.userDetailsEntry.hasError; }, }, created() { @@ -43,7 +44,7 @@ export default { </script> <template> - <div v-if="isLoading"> + <div v-if="isLoaderShown"> <subscription-seat-details-loader /> </div> <gl-table v-else :fields="$options.fields" :items="items" data-testid="seat-usage-details"> diff --git a/ee/app/assets/javascripts/usage_quotas/seats/components/subscription_seats.vue b/ee/app/assets/javascripts/usage_quotas/seats/components/subscription_seats.vue index ea21386f3eff9c73f1737cbc54069aba5ae131a6..16e0bfc28bd6021b07218cf1cb13e6e5a439c2e7 100644 --- a/ee/app/assets/javascripts/usage_quotas/seats/components/subscription_seats.vue +++ b/ee/app/assets/javascripts/usage_quotas/seats/components/subscription_seats.vue @@ -12,6 +12,7 @@ import { GlTable, GlTooltipDirective, GlToggle, + GlSkeletonLoader, } from '@gitlab/ui'; import { mapActions, mapState, mapGetters } from 'vuex'; import { helpPagePath } from '~/helpers/help_page_helper'; @@ -58,10 +59,11 @@ export default { StatisticsCard, StatisticsSeatsCard, SubscriptionUpgradeInfoCard, + GlSkeletonLoader, }, computed: { ...mapState([ - 'isLoading', + 'hasError', 'page', 'perPage', 'total', @@ -86,7 +88,7 @@ export default { 'previewFreeUserCap', 'activeTrial', ]), - ...mapGetters(['tableItems']), + ...mapGetters(['tableItems', 'isLoading']), currentPage: { get() { return this.page; @@ -171,6 +173,9 @@ export default { hasLimitedPlanOrPreviewLimitedPlan() { return this.hasLimitedFreePlan || this.previewFreeUserCap; }, + isLoaderShown() { + return this.isLoading || this.hasError; + }, }, created() { this.fetchBillableMembersList(); @@ -180,7 +185,6 @@ export default { ...mapActions([ 'fetchBillableMembersList', 'fetchGitlabSubscription', - 'resetBillableMembers', 'setBillableMemberToRemove', 'changeMembershipState', 'setSearchQuery', @@ -217,7 +221,7 @@ export default { value: user.membership_state === MEMBER_ACTIVE_STATE, }; - if (this.isLoading) { + if (this.isLoaderShown) { return { ...props, disabled: true }; } @@ -316,30 +320,52 @@ export default { > {{ pendingMembersAlertMessage }} </gl-alert> - <div class="gl-bg-gray-10 gl-display-flex gl-sm-flex-direction-column gl-p-5"> - <statistics-card - :help-link="$options.i18n.seatsInUseLink" - :help-tooltip="seatsInUseTooltipText" - :description="seatsInUseText" - :percentage="seatsInUsePercentage" - :usage-value="String(totalSeatsInUse)" - :total-value="displayedTotalSeats" - class="gl-w-full gl-md-w-half gl-md-mr-5" - /> - <subscription-upgrade-info-card - v-if="showUpgradeInfoCard" - :max-namespace-seats="maxFreeNamespaceSeats" - :explore-plans-path="explorePlansPath" - class="gl-w-full gl-md-w-half gl-md-mt-0 gl-mt-5" - /> - <statistics-seats-card - v-else - :seats-used="maxSeatsUsed" - :seats-owed="seatsOwed" - :purchase-button-link="addSeatsHref" - class="gl-w-full gl-md-w-half gl-md-mt-0 gl-mt-5" - /> + <div class="gl-bg-gray-10 gl-p-5"> + <div + v-if="isLoaderShown" + class="gl-display-grid gl-md-grid-template-columns-2 gl-gap-5" + data-testid="skeleton-loader-cards" + > + <div class="gl-bg-white gl-border gl-p-5 gl-rounded-base"> + <gl-skeleton-loader :height="64"> + <rect width="140" height="30" x="5" y="0" rx="4" /> + <rect width="240" height="10" x="5" y="40" rx="4" /> + <rect width="340" height="10" x="5" y="54" rx="4" /> + </gl-skeleton-loader> + </div> + + <div class="gl-bg-white gl-border gl-p-5 gl-rounded-base"> + <gl-skeleton-loader :height="64"> + <rect width="140" height="30" x="5" y="0" rx="4" /> + <rect width="240" height="10" x="5" y="40" rx="4" /> + <rect width="340" height="10" x="5" y="54" rx="4" /> + </gl-skeleton-loader> + </div> + </div> + + <div v-else class="gl-display-grid gl-md-grid-template-columns-2 gl-gap-5"> + <statistics-card + :help-link="$options.i18n.seatsInUseLink" + :help-tooltip="seatsInUseTooltipText" + :description="seatsInUseText" + :percentage="seatsInUsePercentage" + :usage-value="String(totalSeatsInUse)" + :total-value="displayedTotalSeats" + /> + + <subscription-upgrade-info-card + v-if="showUpgradeInfoCard" + :max-namespace-seats="maxFreeNamespaceSeats" + :explore-plans-path="explorePlansPath" + /> + <statistics-seats-card + v-else + :seats-used="maxSeatsUsed" + :seats-owed="seatsOwed" + :purchase-button-link="addSeatsHref" + /> + </div> </div> <div class="gl-bg-gray-10 gl-p-5 gl-display-flex"> @@ -367,7 +393,7 @@ export default { class="seats-table" :items="tableItems" :fields="fields" - :busy="isLoading" + :busy="isLoaderShown" :show-empty="true" data-testid="table" :empty-text="emptyText" diff --git a/ee/app/assets/javascripts/usage_quotas/seats/store/actions.js b/ee/app/assets/javascripts/usage_quotas/seats/store/actions.js index cc9831caa12dcca6ef120f8266c6a5586f932f33..cb0d8ee3c32e816616ada6714a6e8975b71f8f18 100644 --- a/ee/app/assets/javascripts/usage_quotas/seats/store/actions.js +++ b/ee/app/assets/javascripts/usage_quotas/seats/store/actions.js @@ -48,10 +48,6 @@ export const receiveGitlabSubscriptionError = ({ commit }) => { commit(types.RECEIVE_GITLAB_SUBSCRIPTION_ERROR); }; -export const resetBillableMembers = ({ commit }) => { - commit(types.RESET_BILLABLE_MEMBERS); -}; - export const setBillableMemberToRemove = ({ commit }, member) => { commit(types.SET_BILLABLE_MEMBER_TO_REMOVE, member); }; @@ -64,14 +60,19 @@ export const changeMembershipState = async ({ commit, dispatch, state }, user) = user.membership_state === MEMBER_ACTIVE_STATE ? MEMBER_AWAITING_STATE : MEMBER_ACTIVE_STATE; await GroupsApi.changeMembershipState(state.namespaceId, user.id, newState); - - await dispatch('fetchBillableMembersList'); - await dispatch('fetchGitlabSubscription'); + dispatch('changeMembershipStateSuccess'); } catch { dispatch('changeMembershipStateError'); } }; +export const changeMembershipStateSuccess = ({ commit, dispatch }) => { + dispatch('fetchBillableMembersList'); + dispatch('fetchGitlabSubscription'); + + commit(types.CHANGE_MEMBERSHIP_STATE_SUCCESS); +}; + export const changeMembershipStateError = ({ commit }) => { createAlert({ message: __('Something went wrong. Please try again.'), @@ -80,7 +81,9 @@ export const changeMembershipStateError = ({ commit }) => { commit(types.CHANGE_MEMBERSHIP_STATE_ERROR); }; -export const removeBillableMember = ({ dispatch, state }) => { +export const removeBillableMember = ({ dispatch, state, commit }) => { + commit(types.REMOVE_BILLABLE_MEMBER); + return GroupsApi.removeBillableMemberFromGroup(state.namespaceId, state.billableMemberToRemove.id) .then(() => dispatch('removeBillableMemberSuccess')) .catch(() => dispatch('removeBillableMemberError')); @@ -115,7 +118,7 @@ export const fetchBillableMemberDetails = ({ dispatch, commit, state }, memberId return Promise.resolve(); } - commit(types.FETCH_BILLABLE_MEMBER_DETAILS, memberId); + commit(types.FETCH_BILLABLE_MEMBER_DETAILS, { memberId }); return GroupsApi.fetchBillableGroupMemberMemberships(state.namespaceId, memberId) .then(({ data }) => @@ -125,7 +128,7 @@ export const fetchBillableMemberDetails = ({ dispatch, commit, state }, memberId }; export const fetchBillableMemberDetailsError = ({ commit }, memberId) => { - commit(types.FETCH_BILLABLE_MEMBER_DETAILS_ERROR, memberId); + commit(types.FETCH_BILLABLE_MEMBER_DETAILS_ERROR, { memberId }); createAlert({ message: s__('Billing|An error occurred while getting a billable member details.'), diff --git a/ee/app/assets/javascripts/usage_quotas/seats/store/getters.js b/ee/app/assets/javascripts/usage_quotas/seats/store/getters.js index c667ba719f28acb333260ad6062957c8be0af2c1..d5c354afbda954ec930d83e4a897d4f2321897be 100644 --- a/ee/app/assets/javascripts/usage_quotas/seats/store/getters.js +++ b/ee/app/assets/javascripts/usage_quotas/seats/store/getters.js @@ -8,6 +8,8 @@ export const tableItems = (state) => { })); }; -export const membershipsById = (state) => (memberId) => { - return state.userDetails[memberId] || { isLoading: true, items: [] }; -}; +export const isLoading = (state) => + state.isLoadingBillableMembers || + state.isLoadingGitlabSubscription || + state.isChangingMembershipState || + state.isRemovingBillableMember; diff --git a/ee/app/assets/javascripts/usage_quotas/seats/store/mutation_types.js b/ee/app/assets/javascripts/usage_quotas/seats/store/mutation_types.js index e3afa1f65db64a4ed46e8a55a543981611bf9bce..8f987ea0bbf0e4d15a5150936c8af03b6ec7b0cf 100644 --- a/ee/app/assets/javascripts/usage_quotas/seats/store/mutation_types.js +++ b/ee/app/assets/javascripts/usage_quotas/seats/store/mutation_types.js @@ -10,7 +10,6 @@ export const SET_SEARCH_QUERY = 'SET_SEARCH_QUERY'; export const SET_CURRENT_PAGE = 'SET_CURRENT_PAGE'; export const SET_SORT_OPTION = 'SET_SORT_OPTION'; -export const RESET_BILLABLE_MEMBERS = 'RESET_BILLABLE_MEMBERS'; export const REMOVE_BILLABLE_MEMBER = 'REMOVE_BILLABLE_MEMBER'; export const REMOVE_BILLABLE_MEMBER_SUCCESS = 'REMOVE_BILLABLE_MEMBER_SUCCESS'; export const REMOVE_BILLABLE_MEMBER_ERROR = 'REMOVE_BILLABLE_MEMBER_ERROR'; @@ -21,4 +20,5 @@ export const FETCH_BILLABLE_MEMBER_DETAILS_SUCCESS = 'FETCH_BILLABLE_MEMBER_DETA export const FETCH_BILLABLE_MEMBER_DETAILS_ERROR = 'FETCH_BILLABLE_MEMBER_DETAILS_ERROR'; export const CHANGE_MEMBERSHIP_STATE = 'CHANGE_MEMBERSHIP_STATE'; +export const CHANGE_MEMBERSHIP_STATE_SUCCESS = 'CHANGE_MEMBERSHIP_STATE_SUCCESS'; export const CHANGE_MEMBERSHIP_STATE_ERROR = 'CHANGE_MEMBERSHIP_STATE_ERROR'; diff --git a/ee/app/assets/javascripts/usage_quotas/seats/store/mutations.js b/ee/app/assets/javascripts/usage_quotas/seats/store/mutations.js index 3c2243019cce0093d2fc0673828053c49ad69bff..ab685e88d46b576db00946725769c97942ee7862 100644 --- a/ee/app/assets/javascripts/usage_quotas/seats/store/mutations.js +++ b/ee/app/assets/javascripts/usage_quotas/seats/store/mutations.js @@ -7,27 +7,12 @@ import { import * as types from './mutation_types'; export default { - [types.REQUEST_BILLABLE_MEMBERS](state) { - state.isLoading = true; - state.hasError = false; - }, - + // Gitlab subscription [types.REQUEST_GITLAB_SUBSCRIPTION](state) { - state.isLoading = true; + state.isLoadingGitlabSubscription = true; state.hasError = false; }, - [types.RECEIVE_BILLABLE_MEMBERS_SUCCESS](state, payload) { - const { data, headers } = payload; - state.members = data; - - state.total = Number(headers[HEADER_TOTAL_ENTRIES]); - state.page = Number(headers[HEADER_PAGE_NUMBER]); - state.perPage = Number(headers[HEADER_ITEMS_PER_PAGE]); - - state.isLoading = false; - }, - [types.RECEIVE_GITLAB_SUBSCRIPTION_SUCCESS](state, payload) { const { usage, plan } = payload; @@ -43,19 +28,15 @@ export default { state.hasReachedFreePlanLimit = false; } - state.isLoading = false; - }, - - [types.RECEIVE_BILLABLE_MEMBERS_ERROR](state) { - state.isLoading = false; - state.hasError = true; + state.isLoadingGitlabSubscription = false; }, [types.RECEIVE_GITLAB_SUBSCRIPTION_ERROR](state) { - state.isLoading = false; + state.isLoadingGitlabSubscription = false; state.hasError = true; }, + // Search & Sort [types.SET_SEARCH_QUERY](state, searchString) { state.search = searchString ?? null; }, @@ -68,16 +49,44 @@ export default { state.sort = sortOption; }, - [types.RESET_BILLABLE_MEMBERS](state) { - state.members = []; + // Membership + [types.CHANGE_MEMBERSHIP_STATE](state) { + state.isChangingMembershipState = true; + state.hasError = false; + }, + + [types.CHANGE_MEMBERSHIP_STATE_SUCCESS](state) { + state.isChangingMembershipState = false; + }, + + [types.CHANGE_MEMBERSHIP_STATE_ERROR](state) { + state.isChangingMembershipState = false; + state.hasError = true; + }, + + // Billable member list + [types.REQUEST_BILLABLE_MEMBERS](state) { + state.isLoadingBillableMembers = true; + state.hasError = false; + }, + + [types.RECEIVE_BILLABLE_MEMBERS_SUCCESS](state, payload) { + const { data, headers } = payload; + state.members = data; + + state.total = Number(headers[HEADER_TOTAL_ENTRIES]); + state.page = Number(headers[HEADER_PAGE_NUMBER]); + state.perPage = Number(headers[HEADER_ITEMS_PER_PAGE]); - state.total = null; - state.page = null; - state.perPage = null; + state.isLoadingBillableMembers = false; + }, - state.isLoading = false; + [types.RECEIVE_BILLABLE_MEMBERS_ERROR](state) { + state.isLoadingBillableMembers = false; + state.hasError = true; }, + // Billable member removal [types.SET_BILLABLE_MEMBER_TO_REMOVE](state, memberToRemove) { if (!memberToRemove) { state.billableMemberToRemove = null; @@ -88,33 +97,22 @@ export default { } }, - [types.CHANGE_MEMBERSHIP_STATE](state) { - state.isLoading = true; - state.hasError = false; - }, - - [types.CHANGE_MEMBERSHIP_STATE_ERROR](state) { - state.isLoading = false; - state.hasError = true; - }, - [types.REMOVE_BILLABLE_MEMBER](state) { - state.isLoading = true; + state.isRemovingBillableMember = true; state.hasError = false; }, [types.REMOVE_BILLABLE_MEMBER_SUCCESS](state) { - state.isLoading = false; - state.hasError = false; + state.isRemovingBillableMember = false; state.billableMemberToRemove = null; }, [types.REMOVE_BILLABLE_MEMBER_ERROR](state) { - state.isLoading = false; - state.hasError = true; + state.isRemovingBillableMember = false; state.billableMemberToRemove = null; }, + // Billable member details [types.FETCH_BILLABLE_MEMBER_DETAILS](state, { memberId }) { Vue.set(state.userDetails, memberId, { isLoading: true, diff --git a/ee/app/assets/javascripts/usage_quotas/seats/store/state.js b/ee/app/assets/javascripts/usage_quotas/seats/store/state.js index c1888bd397eb4b412e676c09d5c64c6a321472b3..065be1b167302d6e2b4fe978bfdacd9f0a68bd68 100644 --- a/ee/app/assets/javascripts/usage_quotas/seats/store/state.js +++ b/ee/app/assets/javascripts/usage_quotas/seats/store/state.js @@ -11,7 +11,10 @@ export default ({ freeUserCapEnabled = false, previewFreeUserCap = false, } = {}) => ({ - isLoading: false, + isLoadingBillableMembers: false, + isLoadingGitlabSubscription: false, + isChangingMembershipState: false, + isRemovingBillableMember: false, hasError: false, namespaceId, namespaceName, diff --git a/ee/spec/frontend/usage_quotas/seats/components/subscription_seat_details_spec.js b/ee/spec/frontend/usage_quotas/seats/components/subscription_seat_details_spec.js index fcd13004cfb6f5c12367843adcb349f07ee5fef5..12be76878d5c0a5d118f5bc04438f4a36ed7e958 100644 --- a/ee/spec/frontend/usage_quotas/seats/components/subscription_seat_details_spec.js +++ b/ee/spec/frontend/usage_quotas/seats/components/subscription_seat_details_spec.js @@ -2,7 +2,6 @@ import { GlTable } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; import Vue from 'vue'; import Vuex from 'vuex'; -import Api from 'ee/api'; import SubscriptionSeatDetails from 'ee/usage_quotas/seats/components/subscription_seat_details.vue'; import SubscriptionSeatDetailsLoader from 'ee/usage_quotas/seats/components/subscription_seat_details_loader.vue'; import createStore from 'ee/usage_quotas/seats/store'; @@ -18,12 +17,24 @@ describe('SubscriptionSeatDetails', () => { fetchBillableMemberDetails: jest.fn(), }; - const createComponent = () => { - const store = createStore(initState({ namespaceId: 1, isLoading: true })); + const createComponent = ({ initialUserDetails } = { initialUserDetails: {} }) => { + const seatMemberId = 1; + const store = createStore(initState({ namespaceId: 1 })); + store.state = { + ...store.state, + userDetails: { + [seatMemberId]: { + isLoading: false, + hasError: false, + items: mockMemberDetails, + ...initialUserDetails, + }, + }, + }; wrapper = shallowMount(SubscriptionSeatDetails, { propsData: { - seatMemberId: 1, + seatMemberId, }, store: new Vuex.Store({ ...store, actions }), stubs: { @@ -32,21 +43,43 @@ describe('SubscriptionSeatDetails', () => { }); }; - beforeEach(() => { - Api.fetchBillableGroupMemberMemberships = jest.fn(() => - Promise.resolve({ data: mockMemberDetails }), - ); - createComponent(); - }); - afterEach(() => { wrapper.destroy(); }); describe('on created', () => { + beforeEach(() => { + createComponent(); + }); + it('calls fetchBillableMemberDetails', () => { expect(actions.fetchBillableMemberDetails).toHaveBeenCalledWith(expect.any(Object), 1); }); + }); + + describe('loading state', () => { + beforeEach(() => { + createComponent({ + initialUserDetails: { + isLoading: true, + }, + }); + }); + + it('displays skeleton loader', () => { + expect(wrapper.findComponent(SubscriptionSeatDetailsLoader).isVisible()).toBe(true); + }); + }); + + describe('error state', () => { + beforeEach(() => { + createComponent({ + initialUserDetails: { + isLoading: false, + hasError: true, + }, + }); + }); it('displays skeleton loader', () => { expect(wrapper.findComponent(SubscriptionSeatDetailsLoader).isVisible()).toBe(true); diff --git a/ee/spec/frontend/usage_quotas/seats/components/subscription_seats_spec.js b/ee/spec/frontend/usage_quotas/seats/components/subscription_seats_spec.js index 13b752797ae6792075679d278f8eb4db39c2c049..88344850f7d13947798aaf31d50e4dd21adcf5e9 100644 --- a/ee/spec/frontend/usage_quotas/seats/components/subscription_seats_spec.js +++ b/ee/spec/frontend/usage_quotas/seats/components/subscription_seats_spec.js @@ -27,7 +27,6 @@ Vue.use(Vuex); const actionSpies = { fetchBillableMembersList: jest.fn(), fetchGitlabSubscription: jest.fn(), - resetBillableMembers: jest.fn(), setBillableMemberToRemove: jest.fn(), setSearchQuery: jest.fn(), changeMembershipState: jest.fn(), @@ -50,10 +49,10 @@ const fakeStore = ({ initialState, initialGetters }) => actions: actionSpies, getters: { tableItems: () => mockTableItems, + isLoading: () => false, ...initialGetters, }, state: { - isLoading: false, hasError: false, namespaceId: 1, members: [...mockDataSeats.data], @@ -95,6 +94,7 @@ describe('Subscription Seats', () => { const findStatisticsCard = () => wrapper.findComponent(StatisticsCard); const findStatisticsSeatsCard = () => wrapper.findComponent(StatisticsSeatsCard); const findSubscriptionUpgradeCard = () => wrapper.findComponent(SubscriptionUpgradeInfoCard); + const findSkeletonLoaderCards = () => wrapper.findByTestId('skeleton-loader-cards'); const serializeUser = (rowWrapper) => { const avatarLink = rowWrapper.findComponent(GlAvatarLink); @@ -144,15 +144,15 @@ describe('Subscription Seats', () => { return tableWrapper.findAll('tbody tr').wrappers.map(serializeTableRow); }; + afterEach(() => { + wrapper.destroy(); + }); + describe('actions', () => { beforeEach(() => { wrapper = createComponent(); }); - afterEach(() => { - wrapper.destroy(); - }); - it('correct actions are called on create', () => { expect(actionSpies.fetchBillableMembersList).toHaveBeenCalled(); }); @@ -168,10 +168,6 @@ describe('Subscription Seats', () => { }); }); - afterEach(() => { - wrapper.destroy(); - }); - describe('export button', () => { it('has the correct href', () => { expect(findExportButton().attributes().href).toBe(providedFields.seatUsageExportPath); @@ -340,17 +336,21 @@ describe('Subscription Seats', () => { }); }); - it('disables the toggles when isLoading=true', () => { + it.each([ + [true, false], + [false, true], + ])('disables the toggles when isLoading=%s and hasError=%s', (isLoading, hasError) => { wrapper = createComponent({ mountFn: mount, initialGetters: { tableItems: () => mockTableItems, + isLoading: () => isLoading, }, initialState: { - isLoading: true, hasNoSubscription: true, hasLimitedFreePlan: true, hasReachedFreePlanLimit: true, + hasError, }, }); @@ -687,17 +687,37 @@ describe('Subscription Seats', () => { }); }); - describe('is loading', () => { - beforeEach(() => { - wrapper = createComponent({ initialState: { isLoading: true } }); - }); + describe('Loading state', () => { + describe('When nothing is loading', () => { + beforeEach(() => { + wrapper = createComponent(); + }); - afterEach(() => { - wrapper.destroy(); + it('displays the table in a non-busy state', () => { + expect(findTable().attributes('busy')).toBe(undefined); + }); }); - it('displays table in loading state', () => { - expect(findTable().attributes('busy')).toBe('true'); + describe.each([ + [true, false], + [false, true], + ])('Busy when isLoading=%s and hasError=%s', (isLoading, hasError) => { + beforeEach(() => { + wrapper = createComponent({ + initialGetters: { isLoading: () => isLoading }, + initialState: { hasError }, + }); + }); + + it('displays loading skeletons instead of statistics cards', () => { + expect(findSkeletonLoaderCards().exists()).toBe(true); + expect(findStatisticsCard().exists()).toBe(false); + expect(findStatisticsSeatsCard().exists()).toBe(false); + }); + + it('displays table in busy state', () => { + expect(findTable().attributes('busy')).toBe('true'); + }); }); }); diff --git a/ee/spec/frontend/usage_quotas/seats/store/actions_spec.js b/ee/spec/frontend/usage_quotas/seats/store/actions_spec.js index 4a0bfa790859f35a77de6c3193320a54068e1cec..449184124d7553f9474b1a862f2b54c9a44a9ade 100644 --- a/ee/spec/frontend/usage_quotas/seats/store/actions_spec.js +++ b/ee/spec/frontend/usage_quotas/seats/store/actions_spec.js @@ -1,4 +1,3 @@ -import MockAdapter from 'axios-mock-adapter'; import * as GroupsApi from 'ee/api/groups_api'; import Api from 'ee/api'; import * as actions from 'ee/usage_quotas/seats/store/actions'; @@ -12,26 +11,19 @@ import { } from 'ee_jest/usage_quotas/seats/mock_data'; import testAction from 'helpers/vuex_action_helper'; import { createAlert, VARIANT_SUCCESS } from '~/flash'; -import axios from '~/lib/utils/axios_utils'; -import httpStatusCodes from '~/lib/utils/http_status'; +jest.mock('ee/api/groups_api'); +jest.mock('ee/api'); jest.mock('~/flash'); -describe('seats actions', () => { +describe('Usage Quotas Seats actions', () => { let state; - let mock; beforeEach(() => { state = State(); - mock = new MockAdapter(axios); - }); - - afterEach(() => { - mock.reset(); }); describe('fetchBillableMembersList', () => { - let spy; const payload = { page: 5, search: 'search string', @@ -40,8 +32,6 @@ describe('seats actions', () => { }; beforeEach(() => { - gon.api_version = 'v4'; - state = Object.assign(state, { namespaceId: 1, page: 5, @@ -52,7 +42,10 @@ describe('seats actions', () => { hasNoSubscription: false, }); - spy = jest.spyOn(GroupsApi, 'fetchBillableGroupMembersList'); + GroupsApi.fetchBillableGroupMembersList.mockResolvedValue({ + data: mockDataSeats.data, + headers: mockDataSeats.headers, + }); }); it('passes correct arguments to Api call', () => { @@ -64,7 +57,10 @@ describe('seats actions', () => { expectedActions: expect.anything(), }); - expect(spy).toBeCalledWith(state.namespaceId, expect.objectContaining(payload)); + expect(GroupsApi.fetchBillableGroupMembersList).toBeCalledWith( + state.namespaceId, + expect.objectContaining(payload), + ); }); it('queries awaiting members when on limited free plan', () => { @@ -82,7 +78,7 @@ describe('seats actions', () => { expectedActions: expect.anything(), }); - expect(spy).toBeCalledWith( + expect(GroupsApi.fetchBillableGroupMembersList).toBeCalledWith( state.namespaceId, expect.objectContaining({ ...payload, include_awaiting_members: true }), ); @@ -103,19 +99,13 @@ describe('seats actions', () => { expectedActions: expect.anything(), }); - expect(spy).toBeCalledWith( + expect(GroupsApi.fetchBillableGroupMembersList).toBeCalledWith( state.namespaceId, expect.objectContaining({ ...payload, include_awaiting_members: true }), ); }); describe('on success', () => { - beforeEach(() => { - mock - .onGet('/api/v4/groups/1/billable_members') - .replyOnce(httpStatusCodes.OK, mockDataSeats.data, mockDataSeats.headers); - }); - it('should dispatch the request and success actions', () => { testAction({ action: actions.fetchBillableMembersList, @@ -132,11 +122,9 @@ describe('seats actions', () => { }); describe('on error', () => { - beforeEach(() => { - mock.onGet('/api/v4/groups/1/billable_members').replyOnce(httpStatusCodes.NOT_FOUND, {}); - }); - it('should dispatch the request and error actions', () => { + GroupsApi.fetchBillableGroupMembersList.mockRejectedValue(); + testAction({ action: actions.fetchBillableMembersList, state, @@ -174,13 +162,11 @@ describe('seats actions', () => { describe('fetchGitlabSubscription', () => { beforeEach(() => { - gon.api_version = 'v4'; state.namespaceId = 1; + Api.userSubscription.mockResolvedValue({ data: mockUserSubscription }); }); it('passes correct arguments to Api call', () => { - const spy = jest.spyOn(Api, 'userSubscription'); - testAction({ action: actions.fetchGitlabSubscription, state, @@ -188,16 +174,10 @@ describe('seats actions', () => { expectedActions: expect.anything(), }); - expect(spy).toBeCalledWith(state.namespaceId); + expect(Api.userSubscription).toBeCalledWith(state.namespaceId); }); describe('on success', () => { - beforeEach(() => { - mock - .onGet('/api/v4/namespaces/1/gitlab_subscription') - .replyOnce(httpStatusCodes.OK, mockUserSubscription); - }); - it('should dispatch the request and success actions', () => { testAction({ action: actions.fetchGitlabSubscription, @@ -214,13 +194,9 @@ describe('seats actions', () => { }); describe('on error', () => { - beforeEach(() => { - mock - .onGet('/api/v4/namespaces/1/gitlab_subscription') - .replyOnce(httpStatusCodes.NOT_FOUND, {}); - }); - it('should dispatch the request and error actions', () => { + Api.userSubscription.mockRejectedValue(); + testAction({ action: actions.fetchGitlabSubscription, state, @@ -256,32 +232,21 @@ describe('seats actions', () => { }); }); - describe('resetBillableMembers', () => { - it('should commit mutation', () => { - testAction({ - action: actions.resetBillableMembers, - state, - expectedMutations: [{ type: types.RESET_BILLABLE_MEMBERS }], - }); - }); - }); - describe('setBillableMemberToRemove', () => { it('should commit the set member mutation', async () => { + const member = { id: 'test' }; + await testAction({ action: actions.setBillableMemberToRemove, + payload: member, state, - expectedMutations: [{ type: types.SET_BILLABLE_MEMBER_TO_REMOVE }], + expectedMutations: [{ type: types.SET_BILLABLE_MEMBER_TO_REMOVE, payload: member }], }); }); }); describe('removeBillableMember', () => { - let groupsApiSpy; - beforeEach(() => { - groupsApiSpy = jest.spyOn(GroupsApi, 'removeBillableMemberFromGroup'); - state = { namespaceId: 1, billableMemberToRemove: { @@ -291,36 +256,35 @@ describe('seats actions', () => { }); describe('on success', () => { - beforeEach(() => { - mock.onDelete('/api/v4/groups/1/billable_members/2').reply(httpStatusCodes.OK); - }); - it('dispatches the removeBillableMemberSuccess action', async () => { + GroupsApi.removeBillableMemberFromGroup.mockResolvedValue(); + await testAction({ action: actions.removeBillableMember, state, expectedActions: [{ type: 'removeBillableMemberSuccess' }], + expectedMutations: [{ type: types.REMOVE_BILLABLE_MEMBER }], }); - expect(groupsApiSpy).toHaveBeenCalled(); + expect(GroupsApi.removeBillableMemberFromGroup).toHaveBeenCalledWith( + state.namespaceId, + state.billableMemberToRemove.id, + ); }); }); describe('on error', () => { - beforeEach(() => { - mock - .onDelete('/api/v4/groups/1/billable_members/2') - .reply(httpStatusCodes.UNPROCESSABLE_ENTITY); - }); - it('dispatches the removeBillableMemberError action', async () => { + GroupsApi.removeBillableMemberFromGroup.mockRejectedValue(); + await testAction({ action: actions.removeBillableMember, state, expectedActions: [{ type: 'removeBillableMemberError' }], + expectedMutations: [{ type: types.REMOVE_BILLABLE_MEMBER }], }); - expect(groupsApiSpy).toHaveBeenCalled(); + expect(GroupsApi.removeBillableMemberFromGroup).toHaveBeenCalled(); }); }); }); @@ -369,31 +333,15 @@ describe('seats actions', () => { user = { id: 2, membership_state: MEMBER_ACTIVE_STATE }; }); - afterEach(() => { - mock.reset(); - }); - - describe('on success', () => { + describe('Group API call', () => { beforeEach(() => { - mock - .onPut('/api/v4/groups/1/members/2/state') - .replyOnce(httpStatusCodes.OK, { success: true }); - - expectedActions = [ - { type: 'fetchBillableMembersList' }, - { type: 'fetchGitlabSubscription' }, - ]; - expectedMutations = [{ type: types.CHANGE_MEMBERSHIP_STATE }]; + + expectedActions = [{ type: 'changeMembershipStateSuccess' }]; }); describe('for an active user', () => { it('passes correct arguments to Api call for an active user', () => { - const spy = jest.spyOn(GroupsApi, 'changeMembershipState'); - - jest.spyOn(GroupsApi, 'fetchBillableGroupMembersList'); - jest.spyOn(Api, 'userSubscription'); - testAction({ action: actions.changeMembershipState, payload: user, @@ -402,14 +350,16 @@ describe('seats actions', () => { expectedActions, }); - expect(spy).toBeCalledWith(state.namespaceId, user.id, MEMBER_AWAITING_STATE); + expect(GroupsApi.changeMembershipState).toBeCalledWith( + state.namespaceId, + user.id, + MEMBER_AWAITING_STATE, + ); }); }); describe('for an awaiting user', () => { it('passes correct arguments to Api call for an active user', () => { - const spy = jest.spyOn(GroupsApi, 'changeMembershipState'); - testAction({ action: actions.changeMembershipState, payload: { ...user, membership_state: MEMBER_AWAITING_STATE }, @@ -418,19 +368,19 @@ describe('seats actions', () => { expectedActions, }); - expect(spy).toBeCalledWith(state.namespaceId, user.id, MEMBER_ACTIVE_STATE); + expect(GroupsApi.changeMembershipState).toBeCalledWith( + state.namespaceId, + user.id, + MEMBER_ACTIVE_STATE, + ); }); }); }); describe('on error', () => { - beforeEach(() => { - mock - .onPut('/api/v4/groups/1/members/2/state') - .replyOnce(httpStatusCodes.UNPROCESSABLE_ENTITY, {}); - }); - it('should dispatch the request and error actions', async () => { + GroupsApi.changeMembershipState.mockRejectedValue(); + await testAction({ action: actions.changeMembershipState, payload: user, @@ -442,8 +392,26 @@ describe('seats actions', () => { }); }); + describe('changeMembershipStateSuccess', () => { + it('should dispatch billable members list and GitLab subscription', () => { + testAction({ + action: actions.changeMembershipStateSuccess, + state, + expectedMutations: [ + { + type: types.CHANGE_MEMBERSHIP_STATE_SUCCESS, + }, + ], + expectedActions: [ + { type: 'fetchBillableMembersList' }, + { type: 'fetchGitlabSubscription' }, + ], + }); + }); + }); + describe('changeMembershipStateError', () => { - it('ccommits mutation and calls createAlert', async () => { + it('commits mutation and calls createAlert', async () => { await testAction({ action: actions.changeMembershipStateError, state, @@ -460,9 +428,7 @@ describe('seats actions', () => { const member = mockDataSeats.data[0]; beforeAll(() => { - GroupsApi.fetchBillableGroupMemberMemberships = jest - .fn() - .mockResolvedValue({ data: mockMemberDetails }); + GroupsApi.fetchBillableGroupMemberMemberships.mockResolvedValue({ data: mockMemberDetails }); }); it('commits fetchBillableMemberDetails', async () => { @@ -471,7 +437,7 @@ describe('seats actions', () => { payload: member.id, state, expectedMutations: [ - { type: types.FETCH_BILLABLE_MEMBER_DETAILS, payload: member.id }, + { type: types.FETCH_BILLABLE_MEMBER_DETAILS, payload: { memberId: member.id } }, { type: types.FETCH_BILLABLE_MEMBER_DETAILS_SUCCESS, payload: { memberId: member.id, memberships: mockMemberDetails }, @@ -480,13 +446,13 @@ describe('seats actions', () => { }); }); - it('calls fetchBillableGroupMemberMemberships api', async () => { + it('calls fetchBillableGroupMemberMemberships API', async () => { await testAction({ action: actions.fetchBillableMemberDetails, payload: member.id, state, expectedMutations: [ - { type: types.FETCH_BILLABLE_MEMBER_DETAILS, payload: member.id }, + { type: types.FETCH_BILLABLE_MEMBER_DETAILS, payload: { memberId: member.id } }, { type: types.FETCH_BILLABLE_MEMBER_DETAILS_SUCCESS, payload: { memberId: member.id, memberships: mockMemberDetails }, @@ -497,13 +463,13 @@ describe('seats actions', () => { expect(GroupsApi.fetchBillableGroupMemberMemberships).toHaveBeenCalledWith(null, 2); }); - it('calls fetchBillableGroupMemberMemberships api only once', async () => { + it('calls fetchBillableGroupMemberMemberships API only once', async () => { await testAction({ action: actions.fetchBillableMemberDetails, payload: member.id, state, expectedMutations: [ - { type: types.FETCH_BILLABLE_MEMBER_DETAILS, payload: member.id }, + { type: types.FETCH_BILLABLE_MEMBER_DETAILS, payload: { memberId: member.id } }, { type: types.FETCH_BILLABLE_MEMBER_DETAILS_SUCCESS, payload: { memberId: member.id, memberships: mockMemberDetails }, @@ -529,34 +495,44 @@ describe('seats actions', () => { }); describe('on API error', () => { - beforeAll(() => { - GroupsApi.fetchBillableGroupMemberMemberships = jest.fn().mockRejectedValue(); - }); - it('dispatches fetchBillableMemberDetailsError', async () => { + GroupsApi.fetchBillableGroupMemberMemberships.mockRejectedValue(); + await testAction({ - action: actions.fetchBillableMemberDetailsError, + action: actions.fetchBillableMemberDetails, + payload: member.id, state, - expectedMutations: [{ type: types.FETCH_BILLABLE_MEMBER_DETAILS_ERROR }], + expectedMutations: [ + { type: types.FETCH_BILLABLE_MEMBER_DETAILS, payload: { memberId: member.id } }, + ], + expectedActions: [{ type: 'fetchBillableMemberDetailsError', payload: member.id }], }); }); }); }); describe('fetchBillableMemberDetailsError', () => { + const memberId = 42; + it('commits fetch billable member details error', async () => { await testAction({ action: actions.fetchBillableMemberDetailsError, + payload: memberId, state, - expectedMutations: [{ type: types.FETCH_BILLABLE_MEMBER_DETAILS_ERROR }], + expectedMutations: [ + { type: types.FETCH_BILLABLE_MEMBER_DETAILS_ERROR, payload: { memberId } }, + ], }); }); it('calls createAlert', async () => { await testAction({ action: actions.fetchBillableMemberDetailsError, + payload: memberId, state, - expectedMutations: [{ type: types.FETCH_BILLABLE_MEMBER_DETAILS_ERROR }], + expectedMutations: [ + { type: types.FETCH_BILLABLE_MEMBER_DETAILS_ERROR, payload: { memberId } }, + ], }); expect(createAlert).toHaveBeenCalledWith({ diff --git a/ee/spec/frontend/usage_quotas/seats/store/getters_spec.js b/ee/spec/frontend/usage_quotas/seats/store/getters_spec.js index 91f3cf2352728dffeb7b0170287cf1f932291a60..5ed68ad4220069c0c580499b25f3a73b91e92bf3 100644 --- a/ee/spec/frontend/usage_quotas/seats/store/getters_spec.js +++ b/ee/spec/frontend/usage_quotas/seats/store/getters_spec.js @@ -1,12 +1,8 @@ import * as getters from 'ee/usage_quotas/seats/store/getters'; import State from 'ee/usage_quotas/seats/store/state'; -import { - mockDataSeats, - mockTableItems, - mockMemberDetails, -} from 'ee_jest/usage_quotas/seats/mock_data'; +import { mockDataSeats, mockTableItems } from 'ee_jest/usage_quotas/seats/mock_data'; -describe('Seat usage table getters', () => { +describe('Usage Quotas Seats getters', () => { let state; beforeEach(() => { @@ -27,28 +23,27 @@ describe('Seat usage table getters', () => { }); }); - describe('membershipsById', () => { - describe('when data is not availlable', () => { - it('returns a base state', () => { - expect(getters.membershipsById(state)(0)).toEqual({ - isLoading: true, - items: [], - }); - }); + describe('isLoading', () => { + beforeEach(() => { + state.isLoadingBillableMembers = false; + state.isLoadingGitlabSubscription = false; + state.isChangingMembershipState = false; + state.isRemovingBillableMember = false; }); - describe('when data is available', () => { - it('returns user details state', () => { - state.userDetails[0] = { - isLoading: false, - items: mockMemberDetails, - }; - - expect(getters.membershipsById(state)(0)).toEqual({ - isLoading: false, - items: mockMemberDetails, - }); - }); + it('returns false if nothing is being loaded', () => { + expect(getters.isLoading(state)).toEqual(false); + }); + + it.each([ + 'isLoadingBillableMembers', + 'isLoadingGitlabSubscription', + 'isChangingMembershipState', + 'isRemovingBillableMember', + ])('returns true if %s is being loaded', (key) => { + state[key] = true; + + expect(getters.isLoading(state)).toEqual(true); }); }); }); diff --git a/ee/spec/frontend/usage_quotas/seats/store/mutations_spec.js b/ee/spec/frontend/usage_quotas/seats/store/mutations_spec.js index 89c5677ee19ed69ed28ebe09317810694a6875d1..75b6971b5056bdc520252532a7a9dd26c49f9704 100644 --- a/ee/spec/frontend/usage_quotas/seats/store/mutations_spec.js +++ b/ee/spec/frontend/usage_quotas/seats/store/mutations_spec.js @@ -7,242 +7,235 @@ import { mockUserSubscription, } from 'ee_jest/usage_quotas/seats/mock_data'; -describe('EE seats module mutations', () => { +describe('Usage Quotas Seats mutations', () => { let state; beforeEach(() => { state = createState(); }); - describe(types.REQUEST_BILLABLE_MEMBERS, () => { - beforeEach(() => { - mutations[types.REQUEST_BILLABLE_MEMBERS](state); - }); + describe('GitLab subscription', () => { + it(types.REQUEST_GITLAB_SUBSCRIPTION, () => { + state.isLoadingGitlabSubscription = false; + state.hasError = true; - it('sets isLoading to true', () => { - expect(state.isLoading).toBe(true); - }); + mutations[types.REQUEST_GITLAB_SUBSCRIPTION](state); - it('sets hasError to false', () => { + expect(state.isLoadingGitlabSubscription).toBe(true); expect(state.hasError).toBe(false); }); - }); - - describe(types.RECEIVE_BILLABLE_MEMBERS_SUCCESS, () => { - beforeEach(() => { - mutations[types.RECEIVE_BILLABLE_MEMBERS_SUCCESS](state, mockDataSeats); - }); - - it('sets state as expected', () => { - expect(state.members).toMatchObject(mockDataSeats.data); - - expect(state.total).toBe(3); - expect(state.page).toBe(1); - expect(state.perPage).toBe(1); - }); - - it('sets isLoading to false', () => { - expect(state.isLoading).toBe(false); - }); - }); - - describe(types.RECEIVE_BILLABLE_MEMBERS_ERROR, () => { - beforeEach(() => { - mutations[types.RECEIVE_BILLABLE_MEMBERS_ERROR](state); - }); - it('sets isLoading to false', () => { - expect(state.isLoading).toBe(false); - }); + describe(types.RECEIVE_GITLAB_SUBSCRIPTION_SUCCESS, () => { + describe('when subscription data is passed', () => { + beforeEach(() => { + state.isLoadingGitlabSubscription = true; - it('sets hasError to true', () => { - expect(state.hasError).toBe(true); - }); - }); + mutations[types.RECEIVE_GITLAB_SUBSCRIPTION_SUCCESS](state, mockUserSubscription); + }); - describe(types.REQUEST_GITLAB_SUBSCRIPTION, () => { - beforeEach(() => { - mutations[types.REQUEST_GITLAB_SUBSCRIPTION](state); - }); + it('sets state as expected', () => { + expect(state).toMatchObject({ + seatsInSubscription: mockUserSubscription.usage.seats_in_subscription, + seatsInUse: mockUserSubscription.usage.seats_in_use, + maxSeatsUsed: mockUserSubscription.usage.max_seats_used, + seatsOwed: mockUserSubscription.usage.seats_owed, + hasReachedFreePlanLimit: false, + isLoadingGitlabSubscription: false, + activeTrial: mockUserSubscription.plan.trial, + }); + }); - it('sets isLoading to true', () => { - expect(state.isLoading).toBe(true); - }); + describe('when hasLimitedFreePlan: true', () => { + it('sets hasReachedFreePlanLimit to false when limit has not been reached', () => { + state = { ...state, hasLimitedFreePlan: true, maxFreeNamespaceSeats: 5 }; - it('sets hasError to false', () => { - expect(state.hasError).toBe(false); - }); - }); + mutations[types.RECEIVE_GITLAB_SUBSCRIPTION_SUCCESS](state, { + ...mockUserSubscription, + usage: { seats_in_use: 4 }, + }); - describe(types.RECEIVE_GITLAB_SUBSCRIPTION_SUCCESS, () => { - describe('when subscription data is passed', () => { - beforeEach(() => { - mutations[types.RECEIVE_GITLAB_SUBSCRIPTION_SUCCESS](state, mockUserSubscription); - }); + expect(state.hasReachedFreePlanLimit).toBe(false); + }); - it('sets state as expected', () => { - expect(state.seatsInSubscription).toBe(mockUserSubscription.usage.seats_in_subscription); - expect(state.seatsInUse).toBe(mockUserSubscription.usage.seats_in_use); - expect(state.maxSeatsUsed).toBe(mockUserSubscription.usage.max_seats_used); - expect(state.seatsOwed).toBe(mockUserSubscription.usage.seats_owed); - expect(state.activeTrial).toBe(mockUserSubscription.plan.trial); - expect(state.hasReachedFreePlanLimit).toBe(false); - }); + it('sets hasReachedFreePlanLimit to true when limit has been reached', () => { + state = { ...state, hasLimitedFreePlan: true, maxFreeNamespaceSeats: 5 }; - describe('when hasLimitedFreePlan: true', () => { - it('sets hasReachedFreePlanLimit to false when limit has not been reached', () => { - state = { ...state, hasLimitedFreePlan: true, maxFreeNamespaceSeats: 5 }; + mutations[types.RECEIVE_GITLAB_SUBSCRIPTION_SUCCESS](state, { + ...mockUserSubscription, + usage: { seats_in_use: 5 }, + }); - mutations[types.RECEIVE_GITLAB_SUBSCRIPTION_SUCCESS](state, { - ...mockUserSubscription, - usage: { seats_in_use: 4 }, + expect(state.hasReachedFreePlanLimit).toBe(true); }); - - expect(state.hasReachedFreePlanLimit).toBe(false); }); - it('sets hasReachedFreePlanLimit to true when limit has been reached', () => { - state = { ...state, hasLimitedFreePlan: true, maxFreeNamespaceSeats: 5 }; + describe('when plan is on trial', () => { + it('sets activeTrial to true', () => { + mutations[types.RECEIVE_GITLAB_SUBSCRIPTION_SUCCESS](state, { + ...mockUserSubscription, + plan: { trial: true }, + }); - mutations[types.RECEIVE_GITLAB_SUBSCRIPTION_SUCCESS](state, { - ...mockUserSubscription, - usage: { seats_in_use: 5 }, + expect(state.activeTrial).toBe(true); }); - - expect(state.hasReachedFreePlanLimit).toBe(true); }); }); - describe('when plan is on trial', () => { - it('sets activeTrial to true', () => { - mutations[types.RECEIVE_GITLAB_SUBSCRIPTION_SUCCESS](state, { - ...mockUserSubscription, - plan: { trial: true }, - }); + it('defaults values when subscription data is not passed', () => { + state.isLoadingGitlabSubscription = true; - expect(state.activeTrial).toBe(true); - }); - }); + mutations[types.RECEIVE_GITLAB_SUBSCRIPTION_SUCCESS](state, {}); - it('sets isLoading to false', () => { - expect(state.isLoading).toBe(false); + expect(state).toMatchObject({ + seatsInSubscription: 0, + seatsInUse: 0, + maxSeatsUsed: 0, + seatsOwed: 0, + isLoadingGitlabSubscription: false, + activeTrial: false, + }); }); }); - describe('when subscription data is not passed', () => { - beforeEach(() => { - mutations[types.RECEIVE_GITLAB_SUBSCRIPTION_SUCCESS](state, {}); - }); + it(types.RECEIVE_GITLAB_SUBSCRIPTION_ERROR, () => { + state.isLoadingGitlabSubscription = true; + state.hasError = false; - it('sets state as expected', () => { - expect(state.seatsInSubscription).toBe(0); - expect(state.seatsInUse).toBe(0); - expect(state.maxSeatsUsed).toBe(0); - expect(state.seatsOwed).toBe(0); - expect(state.activeTrial).toBe(false); - }); + mutations[types.RECEIVE_GITLAB_SUBSCRIPTION_ERROR](state); - it('sets isLoading to false', () => { - expect(state.isLoading).toBe(false); - }); + expect(state.isLoadingGitlabSubscription).toBe(false); + expect(state.hasError).toBe(true); }); }); - describe(types.RECEIVE_GITLAB_SUBSCRIPTION_ERROR, () => { - beforeEach(() => { - mutations[types.RECEIVE_GITLAB_SUBSCRIPTION_ERROR](state); + describe('Search and sort', () => { + describe(types.SET_SEARCH_QUERY, () => { + it('sets the search state', () => { + state.search = ''; + const SEARCH_STRING = 'a search string'; + mutations[types.SET_SEARCH_QUERY](state, SEARCH_STRING); + + expect(state.search).toBe(SEARCH_STRING); + }); + + it('sets the search state item to null', () => { + state.search = 'a search string'; + mutations[types.SET_SEARCH_QUERY](state); + expect(state.search).toBe(null); + }); }); - it('sets isLoading to false', () => { - expect(state.isLoading).toBe(false); + it(types.SET_CURRENT_PAGE, () => { + state.page = 1; + mutations[types.SET_CURRENT_PAGE](state, 42); + expect(state.page).toBe(42); }); - it('sets hasError to true', () => { - expect(state.hasError).toBe(true); + it(types.SET_SORT_OPTION, () => { + mutations[types.SET_SORT_OPTION](state, 'last_activity_on_desc'); + expect(state.sort).toBe('last_activity_on_desc'); }); }); - describe(types.SET_SEARCH_QUERY, () => { - it('sets the search state', () => { - const SEARCH_STRING = 'a search string'; - - mutations[types.SET_SEARCH_QUERY](state, SEARCH_STRING); - - expect(state.search).toBe(SEARCH_STRING); + describe('Changing membership state', () => { + it(types.CHANGE_MEMBERSHIP_STATE, () => { + state.isChangingMembershipState = false; + state.hasError = true; + mutations[types.CHANGE_MEMBERSHIP_STATE](state); + expect(state).toMatchObject({ + isChangingMembershipState: true, + hasError: false, + }); }); - it('sets the search state item to null', () => { - mutations[types.SET_SEARCH_QUERY](state); + it(types.CHANGE_MEMBERSHIP_STATE_SUCCESS, () => { + state.isChangingMembershipState = true; + mutations[types.CHANGE_MEMBERSHIP_STATE_SUCCESS](state); + expect(state).toMatchObject({ + isChangingMembershipState: false, + }); + }); - expect(state.search).toBe(null); + it(types.CHANGE_MEMBERSHIP_STATE_ERROR, () => { + state.isChangingMembershipState = true; + state.hasError = false; + mutations[types.CHANGE_MEMBERSHIP_STATE_ERROR](state); + expect(state).toMatchObject({ + isChangingMembershipState: false, + hasError: true, + }); }); }); - describe(types.RESET_BILLABLE_MEMBERS, () => { - beforeEach(() => { - mutations[types.RECEIVE_BILLABLE_MEMBERS_SUCCESS](state, mockDataSeats); - mutations[types.RESET_BILLABLE_MEMBERS](state); + describe('Billable member list', () => { + it(types.REQUEST_BILLABLE_MEMBERS, () => { + state.isLoadingBillableMembers = false; + state.hasError = true; + mutations[types.REQUEST_BILLABLE_MEMBERS](state); + expect(state).toMatchObject({ + isLoadingBillableMembers: true, + hasError: false, + }); }); - it('resets members state', () => { - expect(state.members).toMatchObject([]); - - expect(state.total).toBeNull(); - expect(state.page).toBeNull(); - expect(state.perPage).toBeNull(); - - expect(state.isLoading).toBe(false); + it(types.RECEIVE_BILLABLE_MEMBERS_SUCCESS, () => { + state.isLoadingBillableMembers = true; + mutations[types.RECEIVE_BILLABLE_MEMBERS_SUCCESS](state, mockDataSeats); + expect(state.members).toMatchObject(mockDataSeats.data); + expect(state).toMatchObject({ + total: 3, + page: 1, + perPage: 1, + isLoadingBillableMembers: false, + }); }); - it('sets isLoading to false', () => { - expect(state.isLoading).toBe(false); + it(types.RECEIVE_BILLABLE_MEMBERS_ERROR, () => { + state.hasError = false; + state.isLoadingBillableMembers = true; + mutations[types.RECEIVE_BILLABLE_MEMBERS_ERROR](state); + expect(state).toMatchObject({ + isLoadingBillableMembers: false, + hasError: true, + }); }); }); - describe('member removal', () => { + describe('Billable member removal', () => { const memberToRemove = mockDataSeats.data[0]; beforeEach(() => { + state.billableMemberToRemove = { id: 42 }; mutations[types.RECEIVE_BILLABLE_MEMBERS_SUCCESS](state, mockDataSeats); }); - describe(types.SET_BILLABLE_MEMBER_TO_REMOVE, () => { - it('sets the member to remove', () => { - mutations[types.SET_BILLABLE_MEMBER_TO_REMOVE](state, memberToRemove); + it(types.SET_BILLABLE_MEMBER_TO_REMOVE, () => { + mutations[types.SET_BILLABLE_MEMBER_TO_REMOVE](state, memberToRemove); - expect(state.billableMemberToRemove).toMatchObject(memberToRemove); - }); + expect(state.billableMemberToRemove).toMatchObject(memberToRemove); }); - describe(types.REMOVE_BILLABLE_MEMBER, () => { - it('sets state to loading', () => { - mutations[types.REMOVE_BILLABLE_MEMBER](state, memberToRemove); + it(types.REMOVE_BILLABLE_MEMBER, () => { + mutations[types.REMOVE_BILLABLE_MEMBER](state, memberToRemove); - expect(state).toMatchObject({ isLoading: true, hasError: false }); - }); + expect(state).toMatchObject({ isRemovingBillableMember: true, hasError: false }); }); - describe(types.REMOVE_BILLABLE_MEMBER_SUCCESS, () => { - it('sets state to successfull', () => { - mutations[types.REMOVE_BILLABLE_MEMBER_SUCCESS](state, memberToRemove); + it(types.REMOVE_BILLABLE_MEMBER_SUCCESS, () => { + mutations[types.REMOVE_BILLABLE_MEMBER_SUCCESS](state, memberToRemove); - expect(state).toMatchObject({ - isLoading: false, - hasError: false, - billableMemberToRemove: null, - }); + expect(state).toMatchObject({ + isRemovingBillableMember: false, + billableMemberToRemove: null, }); }); - describe(types.REMOVE_BILLABLE_MEMBER_ERROR, () => { - it('sets state to errored', () => { - mutations[types.REMOVE_BILLABLE_MEMBER_ERROR](state, memberToRemove); + it(types.REMOVE_BILLABLE_MEMBER_ERROR, () => { + mutations[types.REMOVE_BILLABLE_MEMBER_ERROR](state, memberToRemove); - expect(state).toMatchObject({ - isLoading: false, - hasError: true, - billableMemberToRemove: null, - }); + expect(state).toMatchObject({ + isRemovingBillableMember: false, + billableMemberToRemove: null, }); }); }); @@ -250,57 +243,30 @@ describe('EE seats module mutations', () => { describe('fetching billable member details', () => { const member = mockDataSeats.data[0]; - describe(types.FETCH_BILLABLE_MEMBER_DETAILS, () => { - it('sets the state to loading', () => { - mutations[types.FETCH_BILLABLE_MEMBER_DETAILS](state, { memberId: member.id }); - - expect(state.userDetails).toMatchObject({ - [member.id.toString()]: { - isLoading: true, - }, - }); - }); + beforeEach(() => { + delete state.userDetails[member.id]; }); - describe(types.FETCH_BILLABLE_MEMBER_DETAILS_SUCCESS, () => { - beforeEach(() => { - mutations[types.FETCH_BILLABLE_MEMBER_DETAILS_SUCCESS](state, { - memberId: member.id, - memberships: mockMemberDetails, - }); - }); - - it('sets the state to not loading', () => { - expect(state.userDetails[member.id.toString()].isLoading).toBe(false); - }); + it(types.FETCH_BILLABLE_MEMBER_DETAILS, () => { + mutations[types.FETCH_BILLABLE_MEMBER_DETAILS](state, { memberId: member.id }); - it('sets the memberships to the state', () => { - expect(state.userDetails[member.id.toString()].items).toEqual(mockMemberDetails); - }); + expect(state.userDetails[member.id].isLoading).toBe(true); }); - describe(types.FETCH_BILLABLE_MEMBER_DETAILS_ERROR, () => { - it('sets the state to not loading', () => { - mutations[types.FETCH_BILLABLE_MEMBER_DETAILS_ERROR](state, { memberId: member.id }); - - expect(state.userDetails[member.id.toString()].isLoading).toBe(false); + it(types.FETCH_BILLABLE_MEMBER_DETAILS_SUCCESS, () => { + mutations[types.FETCH_BILLABLE_MEMBER_DETAILS_SUCCESS](state, { + memberId: member.id, + memberships: mockMemberDetails, }); - }); - describe(types.SET_CURRENT_PAGE, () => { - it('sets the page state', () => { - mutations[types.SET_CURRENT_PAGE](state, 1); - - expect(state.page).toBe(1); - }); + expect(state.userDetails[member.id].isLoading).toBe(false); + expect(state.userDetails[member.id].items).toEqual(mockMemberDetails); }); - describe(types.SET_SORT_OPTION, () => { - it('sets the sort state', () => { - mutations[types.SET_SORT_OPTION](state, 'last_activity_on_desc'); + it(types.FETCH_BILLABLE_MEMBER_DETAILS_ERROR, () => { + mutations[types.FETCH_BILLABLE_MEMBER_DETAILS_ERROR](state, { memberId: member.id }); - expect(state.sort).toBe('last_activity_on_desc'); - }); + expect(state.userDetails[member.id].isLoading).toBe(false); }); }); });