Commit 88222f8b authored by Sam Figueroa's avatar Sam Figueroa

Add instrumentation to SaaS New Subscription purchase flow

- Track tax link in order summary

- Track render of SaaS Checkout page

- Add the body data to the checkout layout so it has the proper
  context (category) when triggering the tracking in snowplow.

- Track render of Billing page

- Track clicks on edit steps in checkout

- Fix naming of snakeCasedStep since what was called snakeCasedStep was
  actually dash-case/kebab-case.

- Track subscriptionsDetails events on form transition

- Track billingDetails events on form transition

- Track paymentMethod events on form transition

- Track Confirm Purchase

- Zuora iframe will likely not respond with
  error messages unless something goes very wrong.
  Since all card related errors are processed
  within it's iframe until resolved.

- Refactor Step constants for subscriptions

- Step has been changed to only have to know to emit events the steps
  are responsible for the tracking, this also makes it so Step
  doesn't have to pull in the getters & state to be able to
  pass the data on to the tracking.

- refs:
  https://gitlab.com/gitlab-org/gitlab/-/issues/342853
  https://gitlab.com/gitlab-org/gitlab/-/merge_requests/74828#note_802788391
  https://gitlab.com/gitlab-org/gitlab/-/merge_requests/74828#note_804004486
  https://gitlab.com/gitlab-org/gitlab/-/merge_requests/74828#note_804004490
  https://gitlab.com/gitlab-org/gitlab/-/merge_requests/74828#note_804004492
  https://gitlab.com/gitlab-org/gitlab/-/merge_requests/74828#note_804555436
  https://gitlab.com/gitlab-org/gitlab/-/merge_requests/74828#note_804555439
  https://gitlab.com/gitlab-org/gitlab/-/merge_requests/74828#note_804555440
  https://gitlab.com/gitlab-org/gitlab/-/merge_requests/74828#note_804555442
  https://gitlab.com/gitlab-org/gitlab/-/merge_requests/74828#note_804555444
  https://gitlab.com/gitlab-org/gitlab/-/merge_requests/74828#note_784574426
  https://gitlab.com/gitlab-org/gitlab/-/merge_requests/74828#note_810556340
  https://gitlab.com/gitlab-org/gitlab/-/merge_requests/74828#note_817671640
  https://gitlab.com/gitlab-org/gitlab/-/merge_requests/74828#note_817671638
  https://gitlab.com/gitlab-org/gitlab/-/merge_requests/74828#note_817671634

- Lessons some of the concerns expressed in:
  https://gitlab.com/gitlab-org/gitlab/-/issues/350344
parent 82f1ec0f
......@@ -16,12 +16,18 @@ export const ERROR_LOADING_PAYMENT_FORM = s__(
'Checkout|Failed to load the payment form. Please try again.',
);
// The order of the steps in this array determines the flow of the application
/* eslint-disable @gitlab/require-i18n-strings */
export const STEP_SUBSCRIPTION_DETAILS = 'subscriptionDetails';
export const STEP_BILLING_ADDRESS = 'billingAddress';
export const STEP_PAYMENT_METHOD = 'paymentMethod';
export const STEP_CONFIRM_ORDER = 'confirmOrder';
// The order of the steps in this array determines the flow of the application
export const STEPS = [
{ id: 'subscriptionDetails', __typename: 'Step' },
{ id: 'billingAddress', __typename: 'Step' },
{ id: 'paymentMethod', __typename: 'Step' },
{ id: 'confirmOrder', __typename: 'Step' },
{ id: STEP_SUBSCRIPTION_DETAILS, __typename: 'Step' },
{ id: STEP_BILLING_ADDRESS, __typename: 'Step' },
{ id: STEP_PAYMENT_METHOD, __typename: 'Step' },
{ id: STEP_CONFIRM_ORDER, __typename: 'Step' },
];
export const TRACK_SUCCESS_MESSAGE = 'Success';
/* eslint-enable @gitlab/require-i18n-strings */
......@@ -3,6 +3,7 @@ import { mapState } from 'vuex';
import ProgressBar from 'ee/registrations/components/progress_bar.vue';
import { STEPS, SUBSCRIPTON_FLOW_STEPS } from 'ee/registrations/constants';
import { s__ } from '~/locale';
import Tracking from '~/tracking';
import BillingAddress from 'jh_else_ee/subscriptions/new/components/checkout/billing_address.vue';
import ConfirmOrder from './checkout/confirm_order.vue';
import PaymentMethod from './checkout/payment_method.vue';
......@@ -10,11 +11,15 @@ import SubscriptionDetails from './checkout/subscription_details.vue';
export default {
components: { ProgressBar, SubscriptionDetails, BillingAddress, PaymentMethod, ConfirmOrder },
mixins: [Tracking.mixin()],
currentStep: STEPS.checkout,
steps: SUBSCRIPTON_FLOW_STEPS,
computed: {
...mapState(['isNewUser']),
},
mounted() {
this.track('render', { label: 'saas_checkout' });
},
i18n: {
checkout: s__('Checkout|Checkout'),
},
......
......@@ -2,10 +2,11 @@
import { GlFormGroup, GlFormInput, GlFormSelect } from '@gitlab/ui';
import { isEmpty } from 'lodash';
import { mapState, mapActions } from 'vuex';
import { STEPS } from 'ee/subscriptions/constants';
import { STEP_BILLING_ADDRESS } from 'ee/subscriptions/constants';
import Step from 'ee/vue_shared/purchase_flow/components/step.vue';
import { s__ } from '~/locale';
import autofocusonshow from '~/vue_shared/directives/autofocusonshow';
import Tracking from '~/tracking';
export default {
components: {
......@@ -17,6 +18,7 @@ export default {
directives: {
autofocusonshow,
},
mixins: [Tracking.mixin()],
computed: {
...mapState([
'country',
......@@ -117,6 +119,21 @@ export default {
'updateCountryState',
'updateZipCode',
]),
trackStepTransition() {
this.track('click_button', { label: 'select_country', property: this.country });
this.track('click_button', { label: 'state', property: this.countryState });
this.track('click_button', {
label: 'saas_checkout_postal_code',
property: this.zipCode,
});
this.track('click_button', { label: 'continue_payment' });
},
trackStepEdit() {
this.track('click_button', {
label: 'edit',
property: STEP_BILLING_ADDRESS,
});
},
},
i18n: {
stepTitle: s__('Checkout|Billing address'),
......@@ -129,7 +146,7 @@ export default {
stateSelectPrompt: s__('Checkout|Please select a state'),
zipCodeLabel: s__('Checkout|Zip code'),
},
stepId: STEPS[1].id,
stepId: STEP_BILLING_ADDRESS,
};
</script>
<template>
......@@ -138,6 +155,8 @@ export default {
:title="$options.i18n.stepTitle"
:is-valid="isValid"
:next-step-button-text="$options.i18n.nextStepButtonText"
@nextStep="trackStepTransition"
@stepEdit="trackStepEdit"
>
<template #body>
<gl-form-group :label="$options.i18n.countryLabel" label-size="sm" class="mb-3">
......
<script>
import { GlSprintf } from '@gitlab/ui';
import { mapState } from 'vuex';
import { STEPS } from 'ee/subscriptions/constants';
import { STEP_PAYMENT_METHOD, TRACK_SUCCESS_MESSAGE } from 'ee/subscriptions/constants';
import Step from 'ee/vue_shared/purchase_flow/components/step.vue';
import { sprintf, s__ } from '~/locale';
import Tracking from '~/tracking';
import Zuora from './zuora.vue';
export default {
......@@ -12,6 +13,7 @@ export default {
Step,
Zuora,
},
mixins: [Tracking.mixin()],
computed: {
...mapState(['paymentMethodId', 'creditCardDetails']),
isValid() {
......@@ -24,18 +26,43 @@ export default {
});
},
},
methods: {
trackStepSuccess() {
this.track('click_button', {
label: 'review_order',
property: TRACK_SUCCESS_MESSAGE,
});
},
trackStepError(errorMessage) {
this.track('click_button', {
label: 'review_order',
property: errorMessage,
});
},
trackStepEdit() {
this.track('click_button', {
label: 'edit',
property: STEP_PAYMENT_METHOD,
});
},
},
i18n: {
stepTitle: s__('Checkout|Payment method'),
creditCardDetails: s__('Checkout|%{cardType} ending in %{lastFourDigits}'),
expirationDate: s__('Checkout|Exp %{expirationMonth}/%{expirationYear}'),
},
stepId: STEPS[2].id,
stepId: STEP_PAYMENT_METHOD,
};
</script>
<template>
<step :step-id="$options.stepId" :title="$options.i18n.stepTitle" :is-valid="isValid">
<step
:step-id="$options.stepId"
:title="$options.i18n.stepTitle"
:is-valid="isValid"
@stepEdit="trackStepEdit"
>
<template #body="props">
<zuora :active="props.active" />
<zuora :active="props.active" @success="trackStepSuccess" @error="trackStepError" />
</template>
<template #summary>
<div class="js-summary-line-1">
......
......@@ -2,11 +2,12 @@
import { GlFormGroup, GlFormSelect, GlFormInput, GlSprintf, GlLink } from '@gitlab/ui';
import { isEmpty } from 'lodash';
import { mapState, mapGetters, mapActions } from 'vuex';
import { STEPS } from 'ee/subscriptions/constants';
import { STEP_SUBSCRIPTION_DETAILS } from 'ee/subscriptions/constants';
import { NEW_GROUP } from 'ee/subscriptions/new/constants';
import Step from 'ee/vue_shared/purchase_flow/components/step.vue';
import { sprintf, s__ } from '~/locale';
import autofocusonshow from '~/vue_shared/directives/autofocusonshow';
import Tracking from '~/tracking';
export default {
components: {
......@@ -20,6 +21,7 @@ export default {
directives: {
autofocusonshow,
},
mixins: [Tracking.mixin()],
computed: {
...mapState([
'availablePlans',
......@@ -33,6 +35,8 @@ export default {
]),
...mapGetters([
'selectedPlanText',
'selectedPlanDetails',
'selectedGroupId',
'isGroupSelected',
'selectedGroupUsers',
'selectedGroupName',
......@@ -134,6 +138,21 @@ export default {
'updateNumberOfUsers',
'updateOrganizationName',
]),
trackStepTransition() {
this.track('click_button', {
label: 'update_plan_type',
property: this.selectedPlanDetails.code,
});
this.track('click_button', { label: 'update_group', property: this.selectedGroupId });
this.track('click_button', { label: 'update_seat_count', property: this.numberOfUsers });
this.track('click_button', { label: 'continue_billing' });
},
trackStepEdit() {
this.track('click_button', {
label: 'edit',
property: STEP_SUBSCRIPTION_DETAILS,
});
},
},
i18n: {
stepTitle: s__('Checkout|Subscription details'),
......@@ -152,7 +171,7 @@ export default {
group: s__('Checkout|Group'),
users: s__('Checkout|Users'),
},
stepId: STEPS[0].id,
stepId: STEP_SUBSCRIPTION_DETAILS,
};
</script>
<template>
......@@ -161,6 +180,8 @@ export default {
:title="$options.i18n.stepTitle"
:is-valid="isValid"
:next-step-button-text="$options.i18n.nextStepButtonText"
@nextStep="trackStepTransition"
@stepEdit="trackStepEdit"
>
<template #body>
<gl-form-group :label="$options.i18n.selectedPlanLabel" label-size="sm" class="mb-3">
......
<script>
import { GlLoadingIcon } from '@gitlab/ui';
import { mapState, mapActions } from 'vuex';
import Tracking from '~/tracking';
import { ZUORA_SCRIPT_URL, ZUORA_IFRAME_OVERRIDE_PARAMS } from 'ee/subscriptions/constants';
export default {
components: {
GlLoadingIcon,
},
mixins: [Tracking.mixin()],
props: {
active: {
type: Boolean,
required: true,
},
},
emits: ['success', 'error'],
computed: {
...mapState([
'paymentFormParams',
......@@ -51,10 +54,18 @@ export default {
this.fetchPaymentFormParams();
}
},
handleZuoraCallback(response) {
this.paymentFormSubmitted(response);
if (response?.success === 'true') {
this.$emit('success');
} else {
this.$emit('error', response?.errorMessage);
}
},
renderZuoraIframe() {
const params = { ...this.paymentFormParams, ...ZUORA_IFRAME_OVERRIDE_PARAMS };
window.Z.runAfterRender(this.zuoraIframeRendered);
window.Z.render(params, {}, this.paymentFormSubmitted);
window.Z.render(params, {}, this.handleZuoraCallback);
},
},
};
......
......@@ -2,6 +2,7 @@
import { GlLink, GlSprintf } from '@gitlab/ui';
import { mapState, mapGetters } from 'vuex';
import { s__ } from '~/locale';
import Tracking from '~/tracking';
import formattingMixins from '../../formatting_mixins';
export default {
......@@ -9,7 +10,7 @@ export default {
GlLink,
GlSprintf,
},
mixins: [formattingMixins],
mixins: [formattingMixins, Tracking.mixin()],
computed: {
...mapState(['startDate', 'taxRate', 'numberOfUsers']),
...mapGetters([
......@@ -81,6 +82,7 @@ export default {
href="https://about.gitlab.com/handbook/tax/#indirect-taxes-management"
target="_blank"
data-testid="tax-help-link"
@click="track('click_button', { label: 'tax_link' })"
>{{ content }}</gl-link
>
</template>
......
......@@ -72,9 +72,9 @@ export default {
throw new Error(JSON.stringify(data.errors));
}
})
.catch((error) =>
createFlash({ message: GENERAL_ERROR_MESSAGE, error, captureError: true }),
)
.catch((error) => {
createFlash({ message: GENERAL_ERROR_MESSAGE, error, captureError: true });
})
.finally(() => {
this.isLoading = false;
});
......
......@@ -41,6 +41,7 @@ export default {
default: '',
},
},
emits: ['nextStep', 'stepEdit'],
data() {
return {
activeStep: {},
......@@ -71,7 +72,7 @@ export default {
const activeIndex = this.stepList.findIndex(({ id }) => id === this.activeStep.id);
return this.isFinished && index < activeIndex;
},
snakeCasedStep() {
dasherizedStep() {
return dasherize(convertToSnakeCase(this.stepId));
},
},
......@@ -93,10 +94,12 @@ export default {
})
.finally(() => {
this.loading = false;
this.$emit('nextStep');
});
},
async edit() {
this.loading = true;
this.$emit('stepEdit', this.stepId);
await this.$apollo
.mutate({
mutation: updateStepMutation,
......@@ -115,7 +118,7 @@ export default {
<template>
<div class="mb-3 mb-lg-5 gl-w-full">
<step-header :title="title" :is-finished="isFinished" />
<div :class="['card', snakeCasedStep]">
<div class="card" :class="dasherizedStep">
<div v-show="isActive" @keyup.enter="nextStep">
<slot name="body" :active="isActive"></slot>
<gl-form-group
......
......@@ -83,7 +83,11 @@ class SubscriptionsController < ApplicationController
group = params[:selected_group] ? current_group : create_group
return not_found if group.nil?
return render json: group.errors.to_json unless group.persisted?
unless group.persisted?
track_purchase message: group.errors.full_messages.to_s
return render json: group.errors.to_json
end
response = Subscriptions::CreateService.new(
current_user,
......@@ -93,7 +97,10 @@ class SubscriptionsController < ApplicationController
).execute
if response[:success]
track_purchase message: 'Success', namespace: group
response[:data] = { location: redirect_location(group) }
else
track_purchase message: response.dig(:data, :errors), namespace: group
end
render json: response[:data]
......@@ -101,6 +108,15 @@ class SubscriptionsController < ApplicationController
private
def track_purchase(message:, namespace: nil)
Gitlab::Tracking.event(self.class.name, 'click_button',
label: 'confirm_purchase',
property: message,
user: current_user,
namespace: namespace
)
end
def redirect_location(group)
return safe_redirect_path(params[:redirect_after_success]) if params[:redirect_after_success]
......
!!! 5
%html.subscriptions-layout-html{ lang: 'en' }
= render 'layouts/head'
%body.ui-indigo.d-flex.vh-100
%body.ui-indigo.d-flex.vh-100{ data: body_data }
= render "layouts/header/logo_with_title"
= render "layouts/broadcast"
.container.d-flex.gl-flex-direction-column.m-0
......
......@@ -10,7 +10,7 @@
- if show_plans?(namespace)
- plans = billing_available_plans(plans_data, current_plan)
.billing-plans.gl-mt-7
.billing-plans.gl-mt-7{ data: { track: { action: 'render', label: 'billing' } } }
- plans.each do |plan|
- next if plan.hide_card
- is_default_plan = current_plan.nil? && plan.default?
......
......@@ -264,7 +264,7 @@ RSpec.describe SubscriptionsController do
end
end
describe 'POST #create' do
describe 'POST #create', :snowplow do
subject do
post :create,
params: params,
......@@ -350,6 +350,20 @@ RSpec.describe SubscriptionsController do
expect(Gitlab::Json.parse(response.body)['name']).to match_array([Gitlab::Regex.group_name_regex_message, HtmlSafetyValidator.error_message])
end
it 'tracks errors' do
group.valid?
subject
expect_snowplow_event(
category: 'SubscriptionsController',
label: 'confirm_purchase',
action: 'click_button',
property: group.errors.full_messages.to_s,
user: user,
namespace: nil
)
end
end
end
......@@ -372,6 +386,14 @@ RSpec.describe SubscriptionsController do
subject
expect(response.body).to eq('{"errors":"error message"}')
expect_snowplow_event(
category: 'SubscriptionsController',
label: 'confirm_purchase',
action: 'click_button',
property: 'error message',
user: user,
namespace: group
)
end
end
......@@ -430,6 +452,19 @@ RSpec.describe SubscriptionsController do
expect(response.body).to eq({ location: redirect_after_success }.to_json)
end
it 'tracks the creation of the subscriptions' do
subject
expect_snowplow_event(
category: 'SubscriptionsController',
label: 'confirm_purchase',
action: 'click_button',
property: 'Success',
namespace: selected_group,
user: user
)
end
end
end
......
......@@ -3,6 +3,7 @@ import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import Vuex from 'vuex';
import { mockTracking } from 'helpers/tracking_helper';
import { STEPS } from 'ee/subscriptions/constants';
import BillingAddress from 'ee/subscriptions/new/components/checkout/billing_address.vue';
import { getStoreConfig } from 'ee/subscriptions/new/store';
......@@ -83,6 +84,48 @@ describe('Billing Address', () => {
});
});
describe('tracking', () => {
beforeEach(() => {
store.commit(types.UPDATE_COUNTRY, 'US');
store.commit(types.UPDATE_ZIP_CODE, '10467');
store.commit(types.UPDATE_COUNTRY_STATE, 'NY');
});
it('tracks completion details', () => {
const trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
wrapper.findComponent(Step).vm.$emit('nextStep');
expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_button', {
label: 'select_country',
property: 'US',
});
expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_button', {
label: 'state',
property: 'NY',
});
expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_button', {
label: 'saas_checkout_postal_code',
property: '10467',
});
expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_button', {
label: 'continue_payment',
});
});
it('tracks step edits', async () => {
const trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
wrapper.findComponent(Step).vm.$emit('stepEdit', 'stepID');
await nextTick();
expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_button', {
label: 'edit',
property: 'billingAddress',
});
});
});
describe('validations', () => {
const isStepValid = () => wrapper.findComponent(Step).props('isValid');
......
import Vue from 'vue';
import Vuex from 'vuex';
import { triggerEvent, mockTracking, unmockTracking } from 'helpers/tracking_helper';
import Component from 'ee/subscriptions/new/components/order_summary.vue';
import createStore from 'ee/subscriptions/new/store';
import * as types from 'ee/subscriptions/new/store/mutation_types';
......@@ -9,6 +10,7 @@ describe('Order Summary', () => {
Vue.use(Vuex);
let wrapper;
let trackingSpy;
const availablePlans = [
{ id: 'firstPlanId', code: 'bronze', price_per_year: 48, name: 'bronze plan' },
......@@ -36,9 +38,11 @@ describe('Order Summary', () => {
beforeEach(() => {
createComponent();
trackingSpy = mockTracking(undefined, undefined, jest.spyOn);
});
afterEach(() => {
unmockTracking();
wrapper.destroy();
});
......@@ -188,6 +192,17 @@ describe('Order Summary', () => {
});
describe('tax rate', () => {
describe('tracking', () => {
it('track click on tax_link', () => {
trackingSpy = mockTracking(undefined, findTaxHelpLink().element, jest.spyOn);
triggerEvent(findTaxHelpLink().element);
expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_button', {
label: 'tax_link',
});
});
});
describe('with a tax rate of 0', () => {
it('displays the total amount excluding vat', () => {
expect(wrapper.find('.js-total-ex-vat').exists()).toBe(true);
......
......@@ -2,6 +2,8 @@ import { mount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import Vuex from 'vuex';
import Zuora from 'ee/subscriptions/new/components/checkout/zuora.vue';
import { mockTracking } from 'helpers/tracking_helper';
import { STEPS } from 'ee/subscriptions/constants';
import PaymentMethod from 'ee/subscriptions/new/components/checkout/payment_method.vue';
import createStore from 'ee/subscriptions/new/store';
......@@ -68,4 +70,42 @@ describe('Payment Method', () => {
expect(wrapper.find('.js-summary-line-2').text()).toEqual('Exp 12/09');
});
});
describe('tracking', () => {
it('tracks step completion details', async () => {
const trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
wrapper.findComponent(Zuora).vm.$emit('success');
await nextTick();
expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_button', {
label: 'review_order',
property: 'Success',
});
});
it('tracks zuora errors on step transition', async () => {
const trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
wrapper.findComponent(Zuora).vm.$emit('error', 'This was a mistake.');
await nextTick();
expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_button', {
label: 'review_order',
property: 'This was a mistake.',
});
});
it('tracks step edits', async () => {
const trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
wrapper.findComponent(Step).vm.$emit('stepEdit', 'stepID');
await nextTick();
expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_button', {
label: 'edit',
property: 'paymentMethod',
});
});
});
});
......@@ -2,6 +2,7 @@ import { mount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import Vuex from 'vuex';
import { mockTracking } from 'helpers/tracking_helper';
import { STEPS } from 'ee/subscriptions/constants';
import Component from 'ee/subscriptions/new/components/checkout/subscription_details.vue';
import { NEW_GROUP } from 'ee/subscriptions/new/constants';
......@@ -340,6 +341,58 @@ describe('Subscription Details', () => {
});
});
describe('tracking', () => {
let store;
beforeEach(() => {
const mockApollo = createMockApolloProvider(STEPS);
store = createStore(
createDefaultInitialStoreData({
isNewUser: 'false',
namespaceId: '132',
setupForCompany: 'false',
}),
);
store.commit(types.UPDATE_NUMBER_OF_USERS, 13);
wrapper = createComponent({ apolloProvider: mockApollo, store });
});
it('tracks completion details', async () => {
const trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
wrapper.findComponent(Step).vm.$emit('nextStep');
await nextTick();
expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_button', {
label: 'update_plan_type',
property: 'silver',
});
expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_button', {
label: 'update_seat_count',
property: 13,
});
expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_button', {
label: 'update_group',
property: 132,
});
expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_button', {
label: 'continue_billing',
});
});
it('tracks step edits', async () => {
const trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
wrapper.findComponent(Step).vm.$emit('stepEdit', 'stepID');
await nextTick();
expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_button', {
label: 'edit',
property: 'subscriptionDetails',
});
});
});
describe('validations', () => {
const isStepValid = () => wrapper.findComponent(Step).props('isValid');
let store;
......
......@@ -117,7 +117,25 @@ describe('Zuora', () => {
return nextTick().then(() => {
expect(actionMocks.zuoraIframeRendered).toHaveBeenCalled();
wrapper.vm.handleZuoraCallback();
expect(actionMocks.paymentFormSubmitted).toHaveBeenCalled();
});
});
});
describe('tracking', () => {
it('emits success event on correct response', async () => {
wrapper.vm.handleZuoraCallback({ success: 'true' });
await nextTick();
expect(wrapper.emitted().success.length).toEqual(1);
});
it('emits error with message', async () => {
createComponent();
wrapper.vm.handleZuoraCallback({ errorMessage: '1337' });
await nextTick();
expect(wrapper.emitted().error.length).toEqual(1);
expect(wrapper.emitted().error[0]).toEqual(['1337']);
});
});
});
......@@ -4,6 +4,7 @@ import Vuex from 'vuex';
import ProgressBar from 'ee/registrations/components/progress_bar.vue';
import Component from 'ee/subscriptions/new/components/checkout.vue';
import createStore from 'ee/subscriptions/new/store';
import { mockTracking } from 'helpers/tracking_helper';
describe('Checkout', () => {
Vue.use(Vuex);
......@@ -58,4 +59,16 @@ describe('Checkout', () => {
expect(findProgressBar().props('currentStep')).toEqual('Checkout');
});
});
describe('tracking', () => {
it('tracks render on mount', () => {
const trackingSpy = mockTracking(undefined, undefined, jest.spyOn);
shallowMount(Component, { store: createStore() });
expect(trackingSpy).toHaveBeenCalledWith(undefined, 'render', {
label: 'saas_checkout',
});
});
});
});
......@@ -17,7 +17,6 @@ jest.mock('~/flash');
describe('Step', () => {
let wrapper;
const initialProps = {
stepId: STEPS[1].id,
isValid: true,
......@@ -33,6 +32,7 @@ describe('Step', () => {
}
function createComponent(options = {}) {
const { apolloProvider, propsData } = options;
return shallowMount(Step, {
propsData: { ...initialProps, ...propsData },
apolloProvider,
......@@ -198,4 +198,28 @@ describe('Step', () => {
});
});
});
describe('tracking', () => {
it('emits stepEdit', async () => {
const mockApollo = createMockApolloProvider(STEPS, 1);
wrapper = createComponent({ propsData: { stepId: STEPS[1].id }, apolloProvider: mockApollo });
// button in step-summary is not rendered b/c of shallowMount
wrapper.vm.edit();
await waitForPromises();
expect(wrapper.emitted().stepEdit[0]).toEqual(['secondStep']);
});
it('emits nextStep on step transition', async () => {
const mockApollo = createMockApolloProvider(STEPS, 1);
wrapper = createComponent({ propsData: { stepId: STEPS[1].id }, apolloProvider: mockApollo });
await activateFirstStep(mockApollo);
wrapper.findComponent(GlButton).vm.$emit('click');
await waitForPromises();
expect(wrapper.emitted().nextStep).toBeTruthy();
});
});
});
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