Commit a7ca3737 authored by David O'Regan's avatar David O'Regan

Merge branch '321650-mlunoe-migrate-purchase-flow-components-follow-up' into 'master'

Feat(Purchase flow): Add steps error handling

See merge request gitlab-org/gitlab!58084
parents a7c591ee adef44af
<script>
import { GlButton, GlLoadingIcon } from '@gitlab/ui';
import { mapState, mapActions } from 'vuex';
import { GENERAL_ERROR_MESSAGE } from 'ee/vue_shared/purchase_flow/constants';
import activeStepQuery from 'ee/vue_shared/purchase_flow/graphql/queries/active_step.query.graphql';
import createFlash from '~/flash';
import { s__ } from '~/locale';
import { STEPS } from '../../constants';
......@@ -18,7 +20,10 @@ export default {
apollo: {
isActive: {
query: activeStepQuery,
update: ({ activeStep }) => activeStep.id === STEPS[3].id,
update: ({ activeStep }) => activeStep?.id === STEPS[3].id,
error: (error) => {
createFlash({ message: GENERAL_ERROR_MESSAGE, error, captureError: true });
},
},
},
computed: {
......
import Api from 'ee/api';
import { GENERAL_ERROR_MESSAGE } from 'ee/vue_shared/purchase_flow/constants';
import activateNextStepMutation from 'ee/vue_shared/purchase_flow/graphql/mutations/activate_next_step.mutation.graphql';
import createFlash from '~/flash';
import { redirectTo } from '~/lib/utils/url_utility';
......@@ -169,9 +170,13 @@ export const fetchPaymentMethodDetails = ({ state, dispatch, commit }) =>
export const fetchPaymentMethodDetailsSuccess = ({ commit }, creditCardDetails) => {
commit(types.UPDATE_CREDIT_CARD_DETAILS, creditCardDetails);
defaultClient.mutate({
mutation: activateNextStepMutation,
});
defaultClient
.mutate({
mutation: activateNextStepMutation,
})
.catch((error) => {
createFlash({ message: GENERAL_ERROR_MESSAGE, error, captureError: true });
});
};
export const fetchPaymentMethodDetailsError = () => {
......
......@@ -4,7 +4,9 @@ import activateNextStepMutation from 'ee/vue_shared/purchase_flow/graphql/mutati
import updateStepMutation from 'ee/vue_shared/purchase_flow/graphql/mutations/update_active_step.mutation.graphql';
import activeStepQuery from 'ee/vue_shared/purchase_flow/graphql/queries/active_step.query.graphql';
import stepListQuery from 'ee/vue_shared/purchase_flow/graphql/queries/step_list.query.graphql';
import createFlash from '~/flash';
import { convertToSnakeCase, dasherize } from '~/lib/utils/text_utility';
import { GENERAL_ERROR_MESSAGE } from '../constants';
import StepHeader from './step_header.vue';
import StepSummary from './step_summary.vue';
......@@ -44,6 +46,9 @@ export default {
apollo: {
activeStep: {
query: activeStepQuery,
error(error) {
this.handleError(error);
},
},
stepList: {
query: stepListQuery,
......@@ -66,6 +71,9 @@ export default {
},
},
methods: {
handleError(error) {
createFlash({ message: GENERAL_ERROR_MESSAGE, error, captureError: true });
},
async nextStep() {
if (!this.isValid) {
return;
......@@ -75,6 +83,9 @@ export default {
.mutate({
mutation: activateNextStepMutation,
})
.catch((error) => {
this.handleError(error);
})
.finally(() => {
this.loading = false;
});
......@@ -86,6 +97,9 @@ export default {
mutation: updateStepMutation,
variables: { id: this.stepId },
})
.catch((error) => {
this.handleError(error);
})
.finally(() => {
this.loading = false;
});
......
import { s__ } from '~/locale';
export const GENERAL_ERROR_MESSAGE = s__(
'PurchaseStep|An error occured in the purchase step. If the problem persists please contact support@gitlab.com.',
);
---
title: Add error handling feedback inside purchase flow
merge_request: 58084
author:
type: changed
......@@ -6,8 +6,11 @@ import Api from 'ee/api';
import ConfirmOrder from 'ee/subscriptions/new/components/checkout/confirm_order.vue';
import { STEPS } from 'ee/subscriptions/new/constants';
import createStore from 'ee/subscriptions/new/store';
import updateStepMutation from 'ee/vue_shared/purchase_flow/graphql/mutations/update_active_step.mutation.graphql';
import { GENERAL_ERROR_MESSAGE } from 'ee/vue_shared/purchase_flow/constants';
import { createMockApolloProvider } from 'ee_jest/vue_shared/purchase_flow/spec_helper';
import flash from '~/flash';
jest.mock('~/flash');
describe('Confirm Order', () => {
const localVue = createLocalVue();
......@@ -15,19 +18,11 @@ describe('Confirm Order', () => {
localVue.use(VueApollo);
let wrapper;
let mockApolloProvider;
jest.mock('ee/api.js');
const store = createStore();
function activateStep(stepId) {
return mockApolloProvider.clients.defaultClient.mutate({
mutation: updateStepMutation,
variables: { id: stepId },
});
}
function createComponent(options = {}) {
return shallowMount(ConfirmOrder, {
localVue,
......@@ -39,34 +34,34 @@ describe('Confirm Order', () => {
const findConfirmButton = () => wrapper.find(GlButton);
const findLoadingIcon = () => wrapper.find(GlLoadingIcon);
beforeEach(() => {
mockApolloProvider = createMockApolloProvider(STEPS);
wrapper = createComponent({ apolloProvider: mockApolloProvider });
});
afterEach(() => {
wrapper.destroy();
});
describe('Active', () => {
beforeEach(async () => {
await activateStep(STEPS[3].id);
});
describe('when receiving proper step data', () => {
beforeEach(async () => {
const mockApolloProvider = createMockApolloProvider(STEPS, 3);
wrapper = createComponent({ apolloProvider: mockApolloProvider });
});
it('button should be visible', () => {
expect(findConfirmButton().exists()).toBe(true);
});
it('button should be visible', () => {
expect(findConfirmButton().exists()).toBe(true);
});
it('shows the text "Confirm purchase"', () => {
expect(findConfirmButton().text()).toBe('Confirm purchase');
});
it('shows the text "Confirm purchase"', () => {
expect(findConfirmButton().text()).toBe('Confirm purchase');
});
it('the loading indicator should not be visible', () => {
expect(findLoadingIcon().exists()).toBe(false);
it('the loading indicator should not be visible', () => {
expect(findLoadingIcon().exists()).toBe(false);
});
});
describe('Clicking the button', () => {
beforeEach(() => {
const mockApolloProvider = createMockApolloProvider(STEPS, 3);
wrapper = createComponent({ apolloProvider: mockApolloProvider });
Api.confirmOrder = jest.fn().mockReturnValue(new Promise(jest.fn()));
findConfirmButton().vm.$emit('click');
......@@ -84,11 +79,32 @@ describe('Confirm Order', () => {
expect(findLoadingIcon().exists()).toBe(true);
});
});
describe('when failing to receive step data', () => {
beforeEach(async () => {
const mockApolloProvider = createMockApolloProvider([]);
mockApolloProvider.clients.defaultClient.clearStore();
wrapper = createComponent({ apolloProvider: mockApolloProvider });
});
afterEach(() => {
flash.mockClear();
});
it('displays an error', () => {
expect(flash.mock.calls[0][0]).toMatchObject({
message: GENERAL_ERROR_MESSAGE,
captureError: true,
error: expect.any(Error),
});
});
});
});
describe('Inactive', () => {
beforeEach(async () => {
await activateStep(STEPS[1].id);
const mockApolloProvider = createMockApolloProvider(STEPS, 1);
wrapper = createComponent({ apolloProvider: mockApolloProvider });
});
it('button should not be visible', () => {
......
......@@ -3,6 +3,7 @@ import Api from 'ee/api';
import * as constants from 'ee/subscriptions/new/constants';
import defaultClient from 'ee/subscriptions/new/graphql';
import * as actions from 'ee/subscriptions/new/store/actions';
import { GENERAL_ERROR_MESSAGE } from 'ee/vue_shared/purchase_flow/constants';
import activateNextStepMutation from 'ee/vue_shared/purchase_flow/graphql/mutations/activate_next_step.mutation.graphql';
import { useMockLocationHelper } from 'helpers/mock_window_location_helper';
import testAction from 'helpers/vuex_action_helper';
......@@ -33,21 +34,20 @@ describe('Subscriptions Actions', () => {
});
describe('updateSelectedPlan', () => {
it('updates the selected plan', (done) => {
testAction(
it('updates the selected plan', async () => {
await testAction(
actions.updateSelectedPlan,
'planId',
{},
[{ type: 'UPDATE_SELECTED_PLAN', payload: 'planId' }],
[],
done,
);
});
});
describe('updateSelectedGroup', () => {
it('updates the selected group, resets the organization name and updates the number of users', (done) => {
testAction(
it('updates the selected group, resets the organization name and updates the number of users', async () => {
await testAction(
actions.updateSelectedGroup,
'groupId',
{ selectedGroupUsers: 3 },
......@@ -57,191 +57,175 @@ describe('Subscriptions Actions', () => {
{ type: 'UPDATE_NUMBER_OF_USERS', payload: 3 },
],
[],
done,
);
});
});
describe('toggleIsSetupForCompany', () => {
it('toggles the isSetupForCompany value', (done) => {
testAction(
it('toggles the isSetupForCompany value', async () => {
await testAction(
actions.toggleIsSetupForCompany,
{},
{ isSetupForCompany: true },
[{ type: 'UPDATE_IS_SETUP_FOR_COMPANY', payload: false }],
[],
done,
);
});
});
describe('updateNumberOfUsers', () => {
it('updates numberOfUsers to 0 when no value is provided', (done) => {
testAction(
it('updates numberOfUsers to 0 when no value is provided', async () => {
await testAction(
actions.updateNumberOfUsers,
null,
{},
[{ type: 'UPDATE_NUMBER_OF_USERS', payload: 0 }],
[],
done,
);
});
it('updates numberOfUsers when a value is provided', (done) => {
testAction(
it('updates numberOfUsers when a value is provided', async () => {
await testAction(
actions.updateNumberOfUsers,
2,
{},
[{ type: 'UPDATE_NUMBER_OF_USERS', payload: 2 }],
[],
done,
);
});
});
describe('updateOrganizationName', () => {
it('updates organizationName to the provided value', (done) => {
testAction(
it('updates organizationName to the provided value', async () => {
await testAction(
actions.updateOrganizationName,
'name',
{},
[{ type: 'UPDATE_ORGANIZATION_NAME', payload: 'name' }],
[],
done,
);
});
});
describe('fetchCountries', () => {
it('calls fetchCountriesSuccess with the returned data on success', (done) => {
it('calls fetchCountriesSuccess with the returned data on success', async () => {
mock.onGet(countriesPath).replyOnce(200, ['Netherlands', 'NL']);
testAction(
await testAction(
actions.fetchCountries,
null,
{},
[],
[{ type: 'fetchCountriesSuccess', payload: ['Netherlands', 'NL'] }],
done,
);
});
it('calls fetchCountriesError on error', (done) => {
it('calls fetchCountriesError on error', async () => {
mock.onGet(countriesPath).replyOnce(500);
testAction(actions.fetchCountries, null, {}, [], [{ type: 'fetchCountriesError' }], done);
await testAction(actions.fetchCountries, null, {}, [], [{ type: 'fetchCountriesError' }]);
});
});
describe('fetchCountriesSuccess', () => {
it('transforms and adds fetched countryOptions', (done) => {
testAction(
it('transforms and adds fetched countryOptions', async () => {
await testAction(
actions.fetchCountriesSuccess,
[['Netherlands', 'NL']],
{},
[{ type: 'UPDATE_COUNTRY_OPTIONS', payload: [{ text: 'Netherlands', value: 'NL' }] }],
[],
done,
);
});
it('adds an empty array when no data provided', (done) => {
testAction(
it('adds an empty array when no data provided', async () => {
await testAction(
actions.fetchCountriesSuccess,
undefined,
{},
[{ type: 'UPDATE_COUNTRY_OPTIONS', payload: [] }],
[],
done,
);
});
});
describe('fetchCountriesError', () => {
it('creates a flash', (done) => {
testAction(actions.fetchCountriesError, null, {}, [], [], () => {
expect(createFlash).toHaveBeenCalledWith({
message: 'Failed to load countries. Please try again.',
});
done();
it('creates a flash', async () => {
await testAction(actions.fetchCountriesError, null, {}, [], []);
expect(createFlash).toHaveBeenCalledWith({
message: 'Failed to load countries. Please try again.',
});
});
});
describe('fetchStates', () => {
it('calls resetStates and fetchStatesSuccess with the returned data on success', (done) => {
it('calls resetStates and fetchStatesSuccess with the returned data on success', async () => {
mock
.onGet(countryStatesPath, { params: { country: 'NL' } })
.replyOnce(200, { utrecht: 'UT' });
testAction(
await testAction(
actions.fetchStates,
null,
{ country: 'NL' },
[],
[{ type: 'resetStates' }, { type: 'fetchStatesSuccess', payload: { utrecht: 'UT' } }],
done,
);
});
it('only calls resetStates when no country selected', (done) => {
it('only calls resetStates when no country selected', async () => {
mock.onGet(countryStatesPath).replyOnce(500);
testAction(actions.fetchStates, null, { country: null }, [], [{ type: 'resetStates' }], done);
await testAction(actions.fetchStates, null, { country: null }, [], [{ type: 'resetStates' }]);
});
it('calls resetStates and fetchStatesError on error', (done) => {
it('calls resetStates and fetchStatesError on error', async () => {
mock.onGet(countryStatesPath).replyOnce(500);
testAction(
await testAction(
actions.fetchStates,
null,
{ country: 'NL' },
[],
[{ type: 'resetStates' }, { type: 'fetchStatesError' }],
done,
);
});
});
describe('fetchStatesSuccess', () => {
it('transforms and adds received stateOptions', (done) => {
testAction(
it('transforms and adds received stateOptions', async () => {
await testAction(
actions.fetchStatesSuccess,
{ Utrecht: 'UT' },
{},
[{ type: 'UPDATE_STATE_OPTIONS', payload: [{ text: 'Utrecht', value: 'UT' }] }],
[],
done,
);
});
it('adds an empty array when no data provided', (done) => {
testAction(
it('adds an empty array when no data provided', async () => {
await testAction(
actions.fetchStatesSuccess,
undefined,
{},
[{ type: 'UPDATE_STATE_OPTIONS', payload: [] }],
[],
done,
);
});
});
describe('fetchStatesError', () => {
it('creates a flash', (done) => {
testAction(actions.fetchStatesError, null, {}, [], [], () => {
expect(createFlash).toHaveBeenCalledWith({
message: 'Failed to load states. Please try again.',
});
done();
it('creates a flash', async () => {
await testAction(actions.fetchStatesError, null, {}, [], []);
expect(createFlash).toHaveBeenCalledWith({
message: 'Failed to load states. Please try again.',
});
});
});
describe('resetStates', () => {
it('resets the selected state and sets the stateOptions to the initial value', (done) => {
testAction(
it('resets the selected state and sets the stateOptions to the initial value', async () => {
await testAction(
actions.resetStates,
null,
{},
......@@ -250,357 +234,343 @@ describe('Subscriptions Actions', () => {
{ type: 'UPDATE_STATE_OPTIONS', payload: [] },
],
[],
done,
);
});
});
describe('updateCountry', () => {
it('updates country to the provided value', (done) => {
testAction(
it('updates country to the provided value', async () => {
await testAction(
actions.updateCountry,
'country',
{},
[{ type: 'UPDATE_COUNTRY', payload: 'country' }],
[],
done,
);
});
});
describe('updateStreetAddressLine1', () => {
it('updates streetAddressLine1 to the provided value', (done) => {
testAction(
it('updates streetAddressLine1 to the provided value', async () => {
await testAction(
actions.updateStreetAddressLine1,
'streetAddressLine1',
{},
[{ type: 'UPDATE_STREET_ADDRESS_LINE_ONE', payload: 'streetAddressLine1' }],
[],
done,
);
});
});
describe('updateStreetAddressLine2', () => {
it('updates streetAddressLine2 to the provided value', (done) => {
testAction(
it('updates streetAddressLine2 to the provided value', async () => {
await testAction(
actions.updateStreetAddressLine2,
'streetAddressLine2',
{},
[{ type: 'UPDATE_STREET_ADDRESS_LINE_TWO', payload: 'streetAddressLine2' }],
[],
done,
);
});
});
describe('updateCity', () => {
it('updates city to the provided value', (done) => {
testAction(
it('updates city to the provided value', async () => {
await testAction(
actions.updateCity,
'city',
{},
[{ type: 'UPDATE_CITY', payload: 'city' }],
[],
done,
);
});
});
describe('updateCountryState', () => {
it('updates countryState to the provided value', (done) => {
testAction(
it('updates countryState to the provided value', async () => {
await testAction(
actions.updateCountryState,
'countryState',
{},
[{ type: 'UPDATE_COUNTRY_STATE', payload: 'countryState' }],
[],
done,
);
});
});
describe('updateZipCode', () => {
it('updates zipCode to the provided value', (done) => {
testAction(
it('updates zipCode to the provided value', async () => {
await testAction(
actions.updateZipCode,
'zipCode',
{},
[{ type: 'UPDATE_ZIP_CODE', payload: 'zipCode' }],
[],
done,
);
});
});
describe('startLoadingZuoraScript', () => {
it('updates isLoadingPaymentMethod to true', (done) => {
testAction(
it('updates isLoadingPaymentMethod to true', async () => {
await testAction(
actions.startLoadingZuoraScript,
undefined,
{},
[{ type: 'UPDATE_IS_LOADING_PAYMENT_METHOD', payload: true }],
[],
done,
);
});
});
describe('fetchPaymentFormParams', () => {
it('fetches paymentFormParams and calls fetchPaymentFormParamsSuccess with the returned data on success', (done) => {
it('fetches paymentFormParams and calls fetchPaymentFormParamsSuccess with the returned data on success', async () => {
mock
.onGet(paymentFormPath, { params: { id: constants.PAYMENT_FORM_ID } })
.replyOnce(200, { token: 'x' });
testAction(
await testAction(
actions.fetchPaymentFormParams,
null,
{},
[],
[{ type: 'fetchPaymentFormParamsSuccess', payload: { token: 'x' } }],
done,
);
});
it('calls fetchPaymentFormParamsError on error', (done) => {
it('calls fetchPaymentFormParamsError on error', async () => {
mock.onGet(paymentFormPath).replyOnce(500);
testAction(
await testAction(
actions.fetchPaymentFormParams,
null,
{},
[],
[{ type: 'fetchPaymentFormParamsError' }],
done,
);
});
});
describe('fetchPaymentFormParamsSuccess', () => {
it('updates paymentFormParams to the provided value when no errors are present', (done) => {
testAction(
it('updates paymentFormParams to the provided value when no errors are present', async () => {
await testAction(
actions.fetchPaymentFormParamsSuccess,
{ token: 'x' },
{},
[{ type: 'UPDATE_PAYMENT_FORM_PARAMS', payload: { token: 'x' } }],
[],
done,
);
});
it('creates a flash when errors are present', (done) => {
testAction(
it('creates a flash when errors are present', async () => {
await testAction(
actions.fetchPaymentFormParamsSuccess,
{ errors: 'error message' },
{},
[],
[],
() => {
expect(createFlash).toHaveBeenCalledWith({
message: 'Credit card form failed to load: error message',
});
done();
},
);
expect(createFlash).toHaveBeenCalledWith({
message: 'Credit card form failed to load: error message',
});
});
});
describe('fetchPaymentFormParamsError', () => {
it('creates a flash', (done) => {
testAction(actions.fetchPaymentFormParamsError, null, {}, [], [], () => {
expect(createFlash).toHaveBeenCalledWith({
message: 'Credit card form failed to load. Please try again.',
});
done();
it('creates a flash', async () => {
await testAction(actions.fetchPaymentFormParamsError, null, {}, [], []);
expect(createFlash).toHaveBeenCalledWith({
message: 'Credit card form failed to load. Please try again.',
});
});
});
describe('zuoraIframeRendered', () => {
it('updates isLoadingPaymentMethod to false', (done) => {
testAction(
it('updates isLoadingPaymentMethod to false', async () => {
await testAction(
actions.zuoraIframeRendered,
undefined,
{},
[{ type: 'UPDATE_IS_LOADING_PAYMENT_METHOD', payload: false }],
[],
done,
);
});
});
describe('paymentFormSubmitted', () => {
describe('on success', () => {
it('calls paymentFormSubmittedSuccess with the refID from the response and updates isLoadingPaymentMethod to true', (done) => {
testAction(
it('calls paymentFormSubmittedSuccess with the refID from the response and updates isLoadingPaymentMethod to true', async () => {
await testAction(
actions.paymentFormSubmitted,
{ success: true, refId: 'id' },
{},
[{ type: 'UPDATE_IS_LOADING_PAYMENT_METHOD', payload: true }],
[{ type: 'paymentFormSubmittedSuccess', payload: 'id' }],
done,
);
});
});
describe('on failure', () => {
it('calls paymentFormSubmittedError with the response', (done) => {
testAction(
it('calls paymentFormSubmittedError with the response', async () => {
await testAction(
actions.paymentFormSubmitted,
{ error: 'foo' },
{},
[],
[{ type: 'paymentFormSubmittedError', payload: { error: 'foo' } }],
done,
);
});
});
});
describe('paymentFormSubmittedSuccess', () => {
it('updates paymentMethodId to the provided value and calls fetchPaymentMethodDetails', (done) => {
testAction(
it('updates paymentMethodId to the provided value and calls fetchPaymentMethodDetails', async () => {
await testAction(
actions.paymentFormSubmittedSuccess,
'id',
{},
[{ type: 'UPDATE_PAYMENT_METHOD_ID', payload: 'id' }],
[{ type: 'fetchPaymentMethodDetails' }],
done,
);
});
});
describe('paymentFormSubmittedError', () => {
it('creates a flash', (done) => {
testAction(
it('creates a flash', async () => {
await testAction(
actions.paymentFormSubmittedError,
{ errorCode: 'codeFromResponse', errorMessage: 'messageFromResponse' },
{},
[],
[],
() => {
expect(createFlash).toHaveBeenCalledWith({
message:
'Submitting the credit card form failed with code codeFromResponse: messageFromResponse',
});
done();
},
);
expect(createFlash).toHaveBeenCalledWith({
message:
'Submitting the credit card form failed with code codeFromResponse: messageFromResponse',
});
});
});
describe('fetchPaymentMethodDetails', () => {
it('fetches paymentMethodDetails and calls fetchPaymentMethodDetailsSuccess with the returned data on success and updates isLoadingPaymentMethod to false', (done) => {
it('fetches paymentMethodDetails and calls fetchPaymentMethodDetailsSuccess with the returned data on success and updates isLoadingPaymentMethod to false', async () => {
mock
.onGet(paymentMethodPath, { params: { id: 'paymentMethodId' } })
.replyOnce(200, { token: 'x' });
testAction(
await testAction(
actions.fetchPaymentMethodDetails,
null,
{ paymentMethodId: 'paymentMethodId' },
[{ type: 'UPDATE_IS_LOADING_PAYMENT_METHOD', payload: false }],
[{ type: 'fetchPaymentMethodDetailsSuccess', payload: { token: 'x' } }],
done,
);
});
it('calls fetchPaymentMethodDetailsError on error and updates isLoadingPaymentMethod to false', (done) => {
it('calls fetchPaymentMethodDetailsError on error and updates isLoadingPaymentMethod to false', async () => {
mock.onGet(paymentMethodPath).replyOnce(500);
testAction(
await testAction(
actions.fetchPaymentMethodDetails,
null,
{},
[{ type: 'UPDATE_IS_LOADING_PAYMENT_METHOD', payload: false }],
[{ type: 'fetchPaymentMethodDetailsError' }],
done,
);
});
});
describe('fetchPaymentMethodDetailsSuccess', () => {
it('updates creditCardDetails to the provided data and calls defaultClient with activateNextStepMutation', (done) => {
testAction(
const creditCardDetails = {
credit_card_type: 'cc_type',
credit_card_mask_number: '************4242',
credit_card_expiration_month: 12,
credit_card_expiration_year: 2019,
};
it('updates creditCardDetails to the provided data and calls defaultClient with activateNextStepMutation', async () => {
await testAction(
actions.fetchPaymentMethodDetailsSuccess,
creditCardDetails,
{},
[
{
type: 'UPDATE_CREDIT_CARD_DETAILS',
payload: creditCardDetails,
},
],
[],
);
expect(defaultClient.mutate).toHaveBeenCalledWith({
mutation: activateNextStepMutation,
});
});
it('displays an error if activateNextStepMutation fails', async () => {
const error = new Error('An error happened!');
jest.spyOn(defaultClient, 'mutate').mockRejectedValue(error);
await testAction(
actions.fetchPaymentMethodDetailsSuccess,
{
credit_card_type: 'cc_type',
credit_card_mask_number: '************4242',
credit_card_expiration_month: 12,
credit_card_expiration_year: 2019,
},
creditCardDetails,
{},
[
{
type: 'UPDATE_CREDIT_CARD_DETAILS',
payload: {
credit_card_type: 'cc_type',
credit_card_mask_number: '************4242',
credit_card_expiration_month: 12,
credit_card_expiration_year: 2019,
},
payload: creditCardDetails,
},
],
[],
() => {
expect(defaultClient.mutate).toHaveBeenCalledWith({
mutation: activateNextStepMutation,
});
done();
},
);
expect(createFlash).toHaveBeenCalledWith({
message: GENERAL_ERROR_MESSAGE,
error,
captureError: true,
});
});
});
describe('fetchPaymentMethodDetailsError', () => {
it('creates a flash', (done) => {
testAction(actions.fetchPaymentMethodDetailsError, null, {}, [], [], () => {
expect(createFlash).toHaveBeenCalledWith({
message: 'Failed to register credit card. Please try again.',
});
done();
it('creates a flash', async () => {
await testAction(actions.fetchPaymentMethodDetailsError, null, {}, [], []);
expect(createFlash).toHaveBeenCalledWith({
message: 'Failed to register credit card. Please try again.',
});
});
});
describe('confirmOrder', () => {
it('calls confirmOrderSuccess with a redirect location on success', (done) => {
it('calls confirmOrderSuccess with a redirect location on success', async () => {
const response = { location: 'x' };
mock.onPost(confirmOrderPath).replyOnce(200, response);
testAction(
await testAction(
actions.confirmOrder,
null,
{},
[{ type: 'UPDATE_IS_CONFIRMING_ORDER', payload: true }],
[{ type: 'confirmOrderSuccess', payload: response }],
done,
);
});
it('calls confirmOrderError with the errors on error', (done) => {
it('calls confirmOrderError with the errors on error', async () => {
mock.onPost(confirmOrderPath).replyOnce(200, { errors: 'errors' });
testAction(
await testAction(
actions.confirmOrder,
null,
{},
[{ type: 'UPDATE_IS_CONFIRMING_ORDER', payload: true }],
[{ type: 'confirmOrderError', payload: '"errors"' }],
done,
);
});
it('calls confirmOrderError on failure', (done) => {
it('calls confirmOrderError on failure', async () => {
mock.onPost(confirmOrderPath).replyOnce(500);
testAction(
await testAction(
actions.confirmOrder,
null,
{},
[{ type: 'UPDATE_IS_CONFIRMING_ORDER', payload: true }],
[{ type: 'confirmOrderError' }],
done,
);
});
});
......@@ -610,45 +580,37 @@ describe('Subscriptions Actions', () => {
const params = { location: 'http://example.com', plan_id: 'x', quantity: 10 };
it('changes the window location', (done) => {
testAction(actions.confirmOrderSuccess, params, {}, [], [], () => {
expect(window.location.assign).toHaveBeenCalledWith('http://example.com');
done();
});
it('changes the window location', async () => {
await testAction(actions.confirmOrderSuccess, params, {}, [], []);
expect(window.location.assign).toHaveBeenCalledWith('http://example.com');
});
});
describe('confirmOrderError', () => {
it('creates a flash with a default message when no error given', (done) => {
testAction(
it('creates a flash with a default message when no error given', async () => {
await testAction(
actions.confirmOrderError,
null,
{},
[{ type: 'UPDATE_IS_CONFIRMING_ORDER', payload: false }],
[],
() => {
expect(createFlash).toHaveBeenCalledWith({
message: 'Failed to confirm your order! Please try again.',
});
done();
},
);
expect(createFlash).toHaveBeenCalledWith({
message: 'Failed to confirm your order! Please try again.',
});
});
it('creates a flash with a the error message when an error is given', (done) => {
testAction(
it('creates a flash with a the error message when an error is given', async () => {
await testAction(
actions.confirmOrderError,
'"Error"',
{},
[{ type: 'UPDATE_IS_CONFIRMING_ORDER', payload: false }],
[],
() => {
expect(createFlash).toHaveBeenCalledWith({
message: 'Failed to confirm your order: "Error". Please try again.',
});
done();
},
);
expect(createFlash).toHaveBeenCalledWith({
message: 'Failed to confirm your order: "Error". Please try again.',
});
});
});
});
......@@ -3,13 +3,18 @@ import { shallowMount, createLocalVue } from '@vue/test-utils';
import VueApollo from 'vue-apollo';
import Step from 'ee/vue_shared/purchase_flow/components/step.vue';
import StepSummary from 'ee/vue_shared/purchase_flow/components/step_summary.vue';
import { GENERAL_ERROR_MESSAGE } from 'ee/vue_shared/purchase_flow/constants';
import updateStepMutation from 'ee/vue_shared/purchase_flow/graphql/mutations/update_active_step.mutation.graphql';
import waitForPromises from 'helpers/wait_for_promises';
import flash from '~/flash';
import { STEPS } from '../mock_data';
import { createMockApolloProvider } from '../spec_helper';
const localVue = createLocalVue();
localVue.use(VueApollo);
jest.mock('~/flash');
describe('Step', () => {
let wrapper;
......@@ -32,11 +37,15 @@ describe('Step', () => {
localVue,
propsData: { ...initialProps, ...propsData },
apolloProvider,
stubs: {
StepSummary,
},
});
}
afterEach(() => {
wrapper.destroy();
flash.mockClear();
});
describe('Step Body', () => {
......@@ -61,7 +70,27 @@ describe('Step', () => {
const mockApollo = createMockApolloProvider(STEPS, 1);
await activateFirstStep(mockApollo);
wrapper = createComponent({ apolloProvider: mockApollo });
expect(wrapper.find(StepSummary).exists()).toBe(true);
expect(wrapper.findComponent(StepSummary).exists()).toBe(true);
});
it('displays an error when editing a wrong step', async () => {
const mockApollo = createMockApolloProvider(STEPS, 1);
await activateFirstStep(mockApollo);
wrapper = createComponent({
propsData: { stepId: 'does not exist' },
apolloProvider: mockApollo,
});
wrapper.findComponent(StepSummary).findComponent(GlButton).vm.$emit('click');
await waitForPromises();
expect(flash.mock.calls).toHaveLength(1);
expect(flash.mock.calls[0][0]).toMatchObject({
message: GENERAL_ERROR_MESSAGE,
captureError: true,
error: expect.any(Error),
});
});
it('should not be shown when this step is not valid and not active', async () => {
......@@ -69,21 +98,21 @@ describe('Step', () => {
await activateFirstStep(mockApollo);
wrapper = createComponent({ propsData: { isValid: false }, apolloProvider: mockApollo });
expect(wrapper.find(StepSummary).exists()).toBe(false);
expect(wrapper.findComponent(StepSummary).exists()).toBe(false);
});
it('should not be shown when this step is valid and active', () => {
const mockApollo = createMockApolloProvider(STEPS, 1);
wrapper = createComponent({ apolloProvider: mockApollo });
expect(wrapper.find(StepSummary).exists()).toBe(false);
expect(wrapper.findComponent(StepSummary).exists()).toBe(false);
});
it('should not be shown when this step is not valid and active', () => {
const mockApollo = createMockApolloProvider(STEPS, 1);
wrapper = createComponent({ propsData: { isValid: false }, apolloProvider: mockApollo });
expect(wrapper.find(StepSummary).exists()).toBe(false);
expect(wrapper.findComponent(StepSummary).exists()).toBe(false);
});
});
......@@ -92,7 +121,7 @@ describe('Step', () => {
const mockApollo = createMockApolloProvider(STEPS, 1);
wrapper = createComponent({ propsData: { stepId: STEPS[0].id }, apolloProvider: mockApollo });
expect(wrapper.find(StepSummary).props('isEditable')).toBe(true);
expect(wrapper.findComponent(StepSummary).props('isEditable')).toBe(true);
});
});
......@@ -102,14 +131,14 @@ describe('Step', () => {
await activateFirstStep(mockApollo);
wrapper = createComponent({ apolloProvider: mockApollo });
expect(wrapper.find(StepSummary).exists()).toBe(true);
expect(wrapper.findComponent(StepSummary).exists()).toBe(true);
});
it('does not show the summary when this step is not finished', () => {
const mockApollo = createMockApolloProvider(STEPS, 1);
wrapper = createComponent({ apolloProvider: mockApollo });
expect(wrapper.find(StepSummary).exists()).toBe(false);
expect(wrapper.findComponent(StepSummary).exists()).toBe(false);
});
});
......@@ -135,7 +164,7 @@ describe('Step', () => {
const mockApollo = createMockApolloProvider(STEPS, 1);
wrapper = createComponent({ propsData: { isValid: false }, apolloProvider: mockApollo });
expect(wrapper.find(GlButton).attributes('disabled')).toBe('true');
expect(wrapper.findComponent(GlButton).attributes('disabled')).toBe('true');
});
it('is enabled when this step is valid', () => {
......@@ -144,5 +173,20 @@ describe('Step', () => {
expect(wrapper.find(GlButton).attributes('disabled')).toBeUndefined();
});
it('displays an error if navigating too far', async () => {
const mockApollo = createMockApolloProvider(STEPS, 2);
wrapper = createComponent({ propsData: { stepId: STEPS[2].id }, apolloProvider: mockApollo });
wrapper.find(GlButton).vm.$emit('click');
await waitForPromises();
expect(flash.mock.calls).toHaveLength(1);
expect(flash.mock.calls[0][0]).toMatchObject({
message: GENERAL_ERROR_MESSAGE,
captureError: true,
error: expect.any(Error),
});
});
});
});
import { createMockClient } from 'mock-apollo-client';
import activateNextStepMutation from 'ee/vue_shared/purchase_flow/graphql/mutations/activate_next_step.mutation.graphql';
import updateStepMutation from 'ee/vue_shared/purchase_flow/graphql/mutations/update_active_step.mutation.graphql';
import activeStepQuery from 'ee/vue_shared/purchase_flow/graphql/queries/active_step.query.graphql';
import stepListQuery from 'ee/vue_shared/purchase_flow/graphql/queries/step_list.query.graphql';
import resolvers from 'ee/vue_shared/purchase_flow/graphql/resolvers';
import typeDefs from 'ee/vue_shared/purchase_flow/graphql/typedefs.graphql';
import { STEPS } from '../mock_data';
import { createMockApolloProvider } from '../spec_helper';
describe('ee/vue_shared/purchase_flow/graphql/resolvers', () => {
let mockClient;
let mockApolloClient;
beforeEach(async () => {
mockClient = createMockClient({ resolvers, typeDefs });
mockClient.cache.writeQuery({
query: stepListQuery,
data: {
stepList: STEPS,
},
});
mockClient.cache.writeQuery({
query: activeStepQuery,
data: {
activeStep: STEPS[0],
},
describe('Query', () => {
beforeEach(async () => {
const mockApollo = createMockApolloProvider(STEPS, 0);
mockApolloClient = mockApollo.clients.defaultClient;
});
});
describe('Query', () => {
describe('stepListQuery', () => {
it('stores the stepList', async () => {
const queryResult = await mockClient.query({ query: stepListQuery });
const queryResult = await mockApolloClient.query({ query: stepListQuery });
expect(queryResult.data.stepList).toMatchObject(
STEPS.map(({ id }) => {
return { id };
......@@ -38,8 +25,8 @@ describe('ee/vue_shared/purchase_flow/graphql/resolvers', () => {
});
it('throws an error when cache is not initiated properly', async () => {
mockClient.clearStore();
await mockClient.query({ query: stepListQuery }).catch((e) => {
mockApolloClient.clearStore();
await mockApolloClient.query({ query: stepListQuery }).catch((e) => {
expect(e instanceof Error).toBe(true);
});
});
......@@ -47,13 +34,13 @@ describe('ee/vue_shared/purchase_flow/graphql/resolvers', () => {
describe('activeStepQuery', () => {
it('stores the activeStep', async () => {
const queryResult = await mockClient.query({ query: activeStepQuery });
const queryResult = await mockApolloClient.query({ query: activeStepQuery });
expect(queryResult.data.activeStep).toMatchObject({ id: STEPS[0].id });
});
it('throws an error when cache is not initiated properly', async () => {
mockClient.clearStore();
await mockClient.query({ query: activeStepQuery }).catch((e) => {
mockApolloClient.clearStore();
await mockApolloClient.query({ query: activeStepQuery }).catch((e) => {
expect(e instanceof Error).toBe(true);
});
});
......@@ -62,18 +49,23 @@ describe('ee/vue_shared/purchase_flow/graphql/resolvers', () => {
describe('Mutation', () => {
describe('updateActiveStep', () => {
beforeEach(async () => {
const mockApollo = createMockApolloProvider(STEPS, 0);
mockApolloClient = mockApollo.clients.defaultClient;
});
it('updates the active step', async () => {
await mockClient.mutate({
await mockApolloClient.mutate({
mutation: updateStepMutation,
variables: { id: STEPS[1].id },
});
const queryResult = await mockClient.query({ query: activeStepQuery });
const queryResult = await mockApolloClient.query({ query: activeStepQuery });
expect(queryResult.data.activeStep).toMatchObject({ id: STEPS[1].id });
});
it('throws an error when STEP is not present', async () => {
const id = 'does not exist';
await mockClient
await mockApolloClient
.mutate({
mutation: updateStepMutation,
variables: { id },
......@@ -84,8 +76,8 @@ describe('ee/vue_shared/purchase_flow/graphql/resolvers', () => {
});
it('throws an error when cache is not initiated properly', async () => {
mockClient.clearStore();
await mockClient
mockApolloClient.clearStore();
await mockApolloClient
.mutate({
mutation: updateStepMutation,
variables: { id: STEPS[1].id },
......@@ -98,19 +90,20 @@ describe('ee/vue_shared/purchase_flow/graphql/resolvers', () => {
describe('activateNextStep', () => {
it('updates the active step to the next', async () => {
await mockClient.mutate({
const mockApollo = createMockApolloProvider(STEPS, 0);
mockApolloClient = mockApollo.clients.defaultClient;
await mockApolloClient.mutate({
mutation: activateNextStepMutation,
});
const queryResult = await mockClient.query({ query: activeStepQuery });
const queryResult = await mockApolloClient.query({ query: activeStepQuery });
expect(queryResult.data.activeStep).toMatchObject({ id: STEPS[1].id });
});
it('throws an error when out of bounds', async () => {
await mockClient.mutate({
mutation: activateNextStepMutation,
});
const mockApollo = createMockApolloProvider(STEPS, 2);
mockApolloClient = mockApollo.clients.defaultClient;
await mockClient
await mockApolloClient
.mutate({
mutation: activateNextStepMutation,
})
......@@ -120,8 +113,8 @@ describe('ee/vue_shared/purchase_flow/graphql/resolvers', () => {
});
it('throws an error when cache is not initiated properly', async () => {
mockClient.clearStore();
await mockClient
mockApolloClient.clearStore();
await mockApolloClient
.mutate({
mutation: activateNextStepMutation,
})
......
export const STEPS = [
{ __typename: 'Step', id: 'firstStep' },
{ __typename: 'Step', id: 'secondStep' },
{ __typename: 'Step', id: 'finalStep' },
];
......@@ -25506,6 +25506,9 @@ msgstr ""
msgid "Purchase more storage"
msgstr ""
msgid "PurchaseStep|An error occured in the purchase step. If the problem persists please contact support@gitlab.com."
msgstr ""
msgid "Push"
msgstr ""
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment