Commit db7f3998 authored by Serhii Yarynovskyi's avatar Serhii Yarynovskyi Committed by Doug Stull

Change Update button to Contact sales

Add group_contact_sales feature flag
to change Upgrade to Ultimate button
to Contact sales button on the groups:show page
parent 8f2499da
......@@ -44,6 +44,7 @@ export const POPOVER = {
trackingEvents: {
popoverShown: { action: 'popover_shown', label: 'trial_status_popover' },
closeBtnClick: { action: CLICK_BUTTON_ACTION, label: 'close_popover' },
contactSalesBtnClick: { action: CLICK_BUTTON_ACTION, label: 'contact_sales' },
upgradeBtnClick: { action: CLICK_BUTTON_ACTION, label: 'upgrade_to_ultimate' },
compareBtnClick: { action: CLICK_BUTTON_ACTION, label: 'compare_all_plans' },
},
......
......@@ -3,6 +3,8 @@ import { GlButton, GlPopover, GlSprintf } from '@gitlab/ui';
import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils';
import { debounce } from 'lodash';
import { removeTrialSuffix } from 'ee/billings/billings_util';
import { shouldHandRaiseLeadButtonMount } from 'ee/hand_raise_leads/hand_raise_lead';
import GitlabExperiment from '~/experimentation/components/gitlab_experiment.vue';
import axios from '~/lib/utils/axios_utils';
import { formatDate } from '~/lib/utils/datetime_utility';
import { sprintf } from '~/locale';
......@@ -29,6 +31,7 @@ export default {
GlButton,
GlPopover,
GlSprintf,
GitlabExperiment,
},
mixins: [trackingMixin],
inject: {
......@@ -43,6 +46,7 @@ export default {
trialEndDate: {},
userCalloutsPath: {},
userCalloutsFeatureId: {},
user: {},
},
data() {
return {
......@@ -66,6 +70,14 @@ export default {
planName: removeTrialSuffix(this.planName),
});
},
trackingPropertyAndValue() {
return {
property: this.forciblyShowing
? TRACKING_PROPERTY_WHEN_FORCED
: TRACKING_PROPERTY_WHEN_VOLUNTARY,
value: this.daysRemaining,
};
},
},
created() {
this.debouncedResize = debounce(() => this.onResize(), resizeEventDebounceMS);
......@@ -85,12 +97,14 @@ export default {
window.removeEventListener(RESIZE_EVENT, this.debouncedResize);
},
methods: {
trackPageAction(eventName) {
const { action, ...options } = trackingEvents[eventName];
this.track(action, { ...options, ...this.trackingPropertyAndValue });
},
onClose() {
this.forciblyShowing = false;
this.show = false;
const { action, ...options } = trackingEvents.closeBtnClick;
this.track(action, { ...options, ...this.trackingPropertyAndValue() });
this.trackPageAction('closeBtnClick');
},
onForciblyShown() {
if (this.userCalloutsPath && this.userCalloutsFeatureId) {
......@@ -108,24 +122,8 @@ export default {
this.updateDisabledState();
},
onShown() {
const { action, ...options } = trackingEvents.popoverShown;
this.track(action, { ...options, ...this.trackingPropertyAndValue() });
},
onUpgradeBtnClick() {
const { action, ...options } = trackingEvents.upgradeBtnClick;
this.track(action, { ...options, ...this.trackingPropertyAndValue() });
},
onCompareBtnClick() {
const { action, ...options } = trackingEvents.compareBtnClick;
this.track(action, { ...options, ...this.trackingPropertyAndValue() });
},
trackingPropertyAndValue() {
return {
property: this.forciblyShowing
? TRACKING_PROPERTY_WHEN_FORCED
: TRACKING_PROPERTY_WHEN_VOLUNTARY,
value: this.daysRemaining,
};
this.trackPageAction('popoverShown');
shouldHandRaiseLeadButtonMount();
},
updateDisabledState() {
this.disabled = disabledBreakpoints.includes(bp.getBreakpointSize());
......@@ -170,18 +168,37 @@ export default {
</gl-sprintf>
<div class="gl-mt-5">
<gitlab-experiment name="group_contact_sales">
<template #control>
<gl-button
:href="purchaseHref"
ref="upgradeBtn"
category="primary"
variant="confirm"
size="small"
class="gl-mb-0"
block
data-testid="upgradeBtn"
@click="onUpgradeBtnClick"
:href="purchaseHref"
@click="trackPageAction('upgradeBtnClick')"
>
<span class="gl-font-sm">{{ upgradeButtonTitle }}</span>
</gl-button>
</template>
<template #candidate>
<div data-testid="contactSalesBtn" @click="trackPageAction('contactSalesBtnClick')">
<div
class="js-hand-raise-lead-button"
:data-namespace-id="user.namespaceId"
:data-user-name="user.userName"
:data-first-name="user.firstName"
:data-last-name="user.lastName"
:data-company-name="user.companyName"
:data-glm-content="user.glmContent"
small
></div>
</div>
</template>
</gitlab-experiment>
<gl-button
:href="plansHref"
......@@ -192,7 +209,7 @@ export default {
block
data-testid="compareBtn"
:title="$options.i18n.compareAllButtonTitle"
@click="onCompareBtnClick"
@click="trackPageAction('compareBtnClick')"
>
<span class="gl-font-sm">{{ $options.i18n.compareAllButtonTitle }}</span>
</gl-button>
......
......@@ -47,6 +47,12 @@ export const initTrialStatusPopover = () => {
trialEndDate,
userCalloutsPath,
userCalloutsFeatureId,
namespaceId,
userName,
firstName,
lastName,
companyName,
glmContent,
} = el.dataset;
return new Vue({
......@@ -63,6 +69,14 @@ export const initTrialStatusPopover = () => {
trialEndDate: new Date(trialEndDate),
userCalloutsPath,
userCalloutsFeatureId,
user: {
namespaceId,
userName,
firstName,
lastName,
companyName,
glmContent,
},
},
render: (createElement) => createElement(TrialStatusPopover),
});
......
......@@ -57,7 +57,7 @@ export default {
autofocusonshow,
},
mixins: [Tracking.mixin()],
inject: ['user'],
inject: ['user', 'small'],
data() {
return {
isLoading: false,
......@@ -233,9 +233,17 @@ export default {
<template>
<div>
<gl-button v-gl-modal.hand-raise-lead :loading="isLoading">
{{ $options.i18n.buttonText }}
<gl-button
v-gl-modal.hand-raise-lead
:loading="isLoading"
:href="small ? '#' : ''"
:variant="small ? 'confirm' : 'default'"
:size="small ? 'small' : 'medium'"
:class="{ 'gl-mb-3 gl-w-full': small }"
>
<span :class="{ 'gl-font-sm': small }">{{ $options.i18n.buttonText }}</span>
</gl-button>
<gl-modal
ref="modal"
modal-id="hand-raise-lead"
......
......@@ -9,6 +9,7 @@ export const initHandRaiseLeadButton = (el) => {
el,
apolloProvider,
provide: {
small: Boolean(el.hasAttribute('small')),
user: {
namespaceId,
userName,
......
......@@ -39,6 +39,7 @@ export default () => {
SecurityDiscoverApp,
},
provide: {
small: false,
user: {
namespaceId,
userName,
......
......@@ -12,7 +12,13 @@ module TrialStatusWidgetHelper
D3_CALLOUT_ID = 'trial_status_reminder_d3'
def trial_status_popover_data_attrs(group)
base_attrs = trial_status_common_data_attrs(group)
hand_raise_attrs = experiment(:group_contact_sales, namespace: group.root_ancestor, user: current_user, sticky_to: current_user) do |e|
e.control { {} }
e.candidate { hand_raise_props(group, glm_content: 'trial-status-show-group') }
end.run
base_attrs = trial_status_common_data_attrs(group).merge(hand_raise_attrs)
base_attrs.merge(
days_remaining: group.trial_days_remaining, # for experiment tracking
group_name: group.name,
......
---
name: group_contact_sales
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/77327
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/348346
milestone: '14.8'
type: experiment
group: group::conversion
default_enabled: false
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`TrialStatusPopover component matches the snapshot 1`] = `
exports[`TrialStatusPopover component group_contact_sales experiment candidate matches the snapshot 1`] = `
<gl-popover-stub
boundary="viewport"
cssclasses=""
delay="[object Object]"
placement="rightbottom"
target="target-element-identifier"
triggers="hover focus"
>
<gl-sprintf-stub
message="Your trial ends on %{boldStart}%{trialEndDate}%{boldEnd}. We hope you’re enjoying the features of GitLab %{planName}. To keep those features after your trial ends, you’ll need to buy a subscription. (You can also choose GitLab Premium if it meets your needs.)"
/>
<div
class="gl-mt-5"
>
<div
data-testid="contactSalesBtn"
>
<div
class="js-hand-raise-lead-button"
data-company-name="companyName"
data-first-name="firstName"
data-glm-content="glmContent"
data-last-name="lastName"
data-namespace-id="namespaceId"
data-user-name="userName"
small=""
/>
</div>
<gl-button-stub
block=""
buttontextclasses=""
category="secondary"
class="gl-mb-0"
data-testid="compareBtn"
href="billing/path-for/group"
icon=""
size="small"
title="Compare all plans"
variant="confirm"
>
<span
class="gl-font-sm"
>
Compare all plans
</span>
</gl-button-stub>
</div>
</gl-popover-stub>
`;
exports[`TrialStatusPopover component group_contact_sales experiment control matches the snapshot 1`] = `
<gl-popover-stub
boundary="viewport"
cssclasses=""
......@@ -22,7 +76,6 @@ exports[`TrialStatusPopover component matches the snapshot 1`] = `
buttontextclasses=""
category="primary"
class="gl-mb-0"
data-testid="upgradeBtn"
href="transactions/new"
icon=""
size="small"
......
......@@ -10,6 +10,8 @@ import {
import TrialStatusPopover from 'ee/contextual_sidebar/components/trial_status_popover.vue';
import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import { stubExperiments } from 'helpers/experimentation_helper';
import GitlabExperiment from '~/experimentation/components/gitlab_experiment.vue';
import axios from '~/lib/utils/axios_utils';
Vue.config.ignoredElements = ['gl-emoji'];
......@@ -22,6 +24,7 @@ describe('TrialStatusPopover component', () => {
const defaultDaysRemaining = 20;
const findGlPopover = () => wrapper.findComponent(GlPopover);
const findByRef = (ref) => wrapper.find({ ref });
const expectTracking = ({ action, ...options } = {}) => {
return expect(trackingSpy).toHaveBeenCalledWith(undefined, action, {
......@@ -31,7 +34,7 @@ describe('TrialStatusPopover component', () => {
});
};
const createComponent = (providers = {}, mountFn = shallowMount) => {
const createComponent = ({ providers = {}, mountFn = shallowMount, stubs = {} } = {}) => {
return extendedWrapper(
mountFn(TrialStatusPopover, {
provide: {
......@@ -46,8 +49,17 @@ describe('TrialStatusPopover component', () => {
trialEndDate: new Date('2021-02-28'),
userCalloutsPath: undefined,
userCalloutsFeatureId: undefined,
user: {
namespaceId: 'namespaceId',
userName: 'userName',
firstName: 'firstName',
lastName: 'lastName',
companyName: 'companyName',
glmContent: 'glmContent',
},
...providers,
},
stubs,
}),
);
};
......@@ -64,35 +76,63 @@ describe('TrialStatusPopover component', () => {
describe('interpolated strings', () => {
it('correctly interpolates them all', () => {
wrapper = createComponent(undefined, mount);
wrapper = createComponent({ providers: undefined, mountFn: mount });
expect(wrapper.text()).not.toMatch(/%{\w+}/);
});
});
it('tracks when the compare button is clicked', () => {
wrapper.findByTestId('compareBtn').vm.$emit('click');
expectTracking(trackingEvents.compareBtnClick);
});
it('does not include the word "Trial" if the plan name includes it', () => {
wrapper = createComponent({ providers: { planName: 'Ultimate Trial' }, mountFn: mount });
const popoverText = wrapper.text();
expect(popoverText).toContain('We hope you’re enjoying the features of GitLab Ultimate.');
expect(popoverText).toMatch(/Upgrade Some Test Group to Ultimate(?! Trial)/);
});
describe('group_contact_sales experiment', () => {
describe('control', () => {
beforeEach(() => {
stubExperiments({ group_contact_sales: 'control' });
wrapper = createComponent({ stubs: { GitlabExperiment } });
trackingSpy = mockTracking('_category_', wrapper.element, jest.spyOn);
});
it('matches the snapshot', () => {
expect(wrapper.element).toMatchSnapshot();
});
it('tracks when the upgrade button is clicked', () => {
wrapper.findByTestId('upgradeBtn').vm.$emit('click');
findByRef('upgradeBtn').vm.$emit('click');
expectTracking(trackingEvents.upgradeBtnClick);
});
});
it('tracks when the compare button is clicked', () => {
wrapper.findByTestId('compareBtn').vm.$emit('click');
expectTracking(trackingEvents.compareBtnClick);
describe('candidate', () => {
beforeEach(() => {
stubExperiments({ group_contact_sales: 'candidate' });
wrapper = createComponent({ stubs: { GitlabExperiment } });
trackingSpy = mockTracking('_category_', wrapper.element, jest.spyOn);
});
it('does not include the word "Trial" if the plan name includes it', () => {
wrapper = createComponent({ planName: 'Ultimate Trial' }, mount);
it('matches the snapshot', () => {
expect(wrapper.element).toMatchSnapshot();
});
const popoverText = wrapper.text();
it('tracks when the contact sales button is clicked', () => {
wrapper.findByTestId('contactSalesBtn').trigger('click');
expect(popoverText).toContain('We hope you’re enjoying the features of GitLab Ultimate.');
expect(popoverText).toMatch(/Upgrade Some Test Group to Ultimate(?! Trial)/);
expectTracking(trackingEvents.contactSalesBtnClick);
});
});
});
describe('startInitiallyShown', () => {
......@@ -107,7 +147,7 @@ describe('TrialStatusPopover component', () => {
describe('when set to true', () => {
beforeEach(() => {
wrapper = createComponent({ startInitiallyShown: true });
wrapper = createComponent({ providers: { startInitiallyShown: true } });
});
it('causes the popover to be shown by default', () => {
......@@ -121,8 +161,10 @@ describe('TrialStatusPopover component', () => {
describe('and the user callout values are provided', () => {
beforeEach(() => {
wrapper = createComponent({
providers: {
startInitiallyShown: true,
...userCalloutProviders,
},
});
});
......@@ -142,7 +184,7 @@ describe('TrialStatusPopover component', () => {
describe('when set to false', () => {
beforeEach(() => {
wrapper = createComponent({ ...userCalloutProviders });
wrapper = createComponent({ providers: { ...userCalloutProviders } });
});
it('does not cause the popover to be shown by default', () => {
......@@ -162,7 +204,7 @@ describe('TrialStatusPopover component', () => {
describe('close button', () => {
describe('when the popover starts off forcibly shown', () => {
beforeEach(() => {
wrapper = createComponent({ startInitiallyShown: true }, mount);
wrapper = createComponent({ providers: { startInitiallyShown: true }, mountFn: mount });
});
it('is rendered', () => {
......@@ -235,7 +277,7 @@ describe('TrialStatusPopover component', () => {
`(
'sets the expected values for `property` & `value`',
({ daysRemaining, startInitiallyShown, property }) => {
wrapper = createComponent({ daysRemaining, startInitiallyShown });
wrapper = createComponent({ providers: { daysRemaining, startInitiallyShown } });
// We'll use the "onShown" method to exercise trackingPropertyAndValue
findGlPopover().vm.$emit('shown');
......
......@@ -23,7 +23,7 @@ describe('HandRaiseLeadButton', () => {
let fakeApollo;
let trackingSpy;
const createComponent = () => {
const createComponent = (small = false) => {
const mockResolvers = {
Query: {
countries() {
......@@ -39,6 +39,7 @@ describe('HandRaiseLeadButton', () => {
return shallowMountExtended(HandRaiseLeadButton, {
apolloProvider: fakeApollo,
provide: {
small,
user: {
namespaceId: '1',
userName: 'joe',
......@@ -71,8 +72,12 @@ describe('HandRaiseLeadButton', () => {
expect(findButton().props('loading')).toBe(false);
});
it('has the "Contact sales" text on the button', () => {
expect(findButton().text()).toBe(PQL_BUTTON_TEXT);
it('has default medium button and the "Contact sales" text on the button', () => {
const button = findButton();
expect(button.props('variant')).toBe('default');
expect(button.props('size')).toBe('medium');
expect(button.text()).toBe(PQL_BUTTON_TEXT);
});
it('has the default injected values', async () => {
......@@ -129,6 +134,17 @@ describe('HandRaiseLeadButton', () => {
label: 'hand_raise_lead_form',
});
});
describe('small button', () => {
it('has small confirm button and the "Contact sales" text on the button', () => {
wrapper = createComponent(true);
const button = findButton();
expect(button.props('variant')).toBe('confirm');
expect(button.props('size')).toBe('small');
expect(button.text()).toBe(PQL_BUTTON_TEXT);
});
});
});
describe('submit button', () => {
......
......@@ -27,6 +27,7 @@ describe('Card security discover app', () => {
propsData,
apolloProvider: createMockApollo([], {}),
provide: {
small: false,
user: {
namespaceId: '1',
userName: 'joe',
......
......@@ -27,6 +27,7 @@ RSpec.describe TrialStatusWidgetHelper, :saas do
trial_starts_on: trial_start_date,
trial_ends_on: trial_end_date
)
stub_experiments(group_contact_sales: :control)
stub_experiments(forcibly_show_trial_status_popover: :candidate)
allow_next_instance_of(GitlabSubscriptions::FetchSubscriptionPlansService, plan: :free) do |instance|
allow(instance).to receive(:execute).and_return([
......@@ -128,6 +129,32 @@ RSpec.describe TrialStatusWidgetHelper, :saas do
it 'records the experiment subject' do
expect { data_attrs }.to change { ExperimentSubject.count }
end
context 'when group_contact_sales is enabled' do
before do
stub_experiments(group_contact_sales: :candidate)
end
it 'returns the needed data attributes for mounting the popover Vue component' do
expect(data_attrs).to match(
shared_expected_attrs.merge(
namespace_id: group.id,
user_name: user.username,
first_name: user.first_name,
last_name: user.last_name,
company_name: user.organization,
glm_content: 'trial-status-show-group',
group_name: group.name,
purchase_href: new_subscriptions_path(namespace_id: group.id, plan_id: 'ultimate-plan-id'),
target_id: shared_expected_attrs[:container_id],
start_initially_shown: false,
trial_end_date: trial_end_date,
user_callouts_path: callouts_path,
user_callouts_feature_id: user_callouts_feature_id
)
)
end
end
end
describe '#trial_status_widget_data_attrs' do
......
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