Commit bdad2e58 authored by Dallas Reedy's avatar Dallas Reedy Committed by James Fargher

Add option to forcibly show the trial status popover

parent 5da48471
......@@ -65,6 +65,8 @@
min-width: 0;
}
// .gl-font-size-inherit will be moved to @gitlab/ui by https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1466
.gl-font-size-inherit,
.font-size-inherit { font-size: inherit; }
.gl-w-8 { width: px-to-rem($grid-size); }
.gl-w-16 { width: px-to-rem($grid-size * 2); }
......@@ -228,3 +230,13 @@ $gl-line-height-42: px-to-rem(42px);
.gl-max-h-none\! {
max-height: none !important;
}
// Will be moved to @gitlab/ui by https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1465
.gl-popover {
.popover-header {
.gl-button.close {
margin-top: -$gl-spacing-scale-3;
margin-right: -$gl-spacing-scale-4;
}
}
}
......@@ -25,6 +25,7 @@ export const WIDGET = {
export const POPOVER = {
i18n: {
close: s__('Modal|Close'),
compareAllButtonTitle: s__('Trials|Compare all plans'),
popoverTitle: s__('Trials|Hey there'),
popoverContent: s__(`Trials|Your trial ends on
......@@ -36,6 +37,7 @@ export const POPOVER = {
},
trackingEvents: {
popoverShown: { action: 'popover_shown', label: 'trial_status_popover' },
closeBtnClick: { action: CLICK_BUTTON_ACTION, label: 'close_popover' },
upgradeBtnClick: { action: CLICK_BUTTON_ACTION, label: 'upgrade_to_ultimate' },
compareBtnClick: { action: CLICK_BUTTON_ACTION, label: 'compare_all_plans' },
},
......
......@@ -29,12 +29,16 @@ export default {
planName: {},
plansHref: {},
purchaseHref: {},
startInitiallyShown: { default: false },
targetId: {},
trialEndDate: {},
},
data() {
return {
disabled: false,
forciblyShowing: false,
showCloseButton: false,
show: false,
};
},
i18n,
......@@ -53,6 +57,12 @@ export default {
created() {
this.debouncedResize = debounce(() => this.onResize(), resizeEventDebounceMS);
window.addEventListener(RESIZE_EVENT, this.debouncedResize);
if (this.startInitiallyShown) {
this.forciblyShowing = true;
this.showCloseButton = true;
this.show = true;
}
},
mounted() {
this.onResize();
......@@ -61,6 +71,13 @@ export default {
window.removeEventListener(RESIZE_EVENT, this.debouncedResize);
},
methods: {
onClose() {
this.forciblyShowing = false;
this.show = false;
const { action, ...options } = this.$options.trackingEvents.closeBtnClick;
this.track(action, options);
},
onResize() {
this.updateDisabledState();
},
......@@ -85,17 +102,30 @@ export default {
<template>
<gl-popover
ref="popover"
:container="containerId"
:target="targetId"
:disabled="disabled"
placement="rightbottom"
boundary="viewport"
:delay="{ hide: 400 }"
:show.sync="show"
:triggers="forciblyShowing ? '' : 'hover focus'"
@shown="onShown"
>
<template #title>
<gl-button
v-if="showCloseButton"
category="tertiary"
class="close"
data-testid="closeBtn"
:aria-label="$options.i18n.close"
@click.prevent="onClose"
>
<span class="gl-display-inline-block" aria-hidden="true">&times;</span>
</gl-button>
{{ $options.i18n.popoverTitle }}
<gl-emoji class="gl-vertical-align-baseline font-size-inherit gl-ml-1" data-name="wave" />
<gl-emoji class="gl-vertical-align-baseline gl-font-size-inherit gl-ml-1" data-name="wave" />
</template>
<gl-sprintf :message="$options.i18n.popoverContent">
......
......@@ -41,6 +41,7 @@ export const initTrialStatusPopover = () => {
planName,
plansHref,
purchaseHref,
startInitiallyShown,
targetId,
trialEndDate,
} = el.dataset;
......@@ -53,6 +54,7 @@ export const initTrialStatusPopover = () => {
planName,
plansHref,
purchaseHref,
startInitiallyShown: startInitiallyShown !== undefined,
targetId,
trialEndDate: new Date(trialEndDate),
},
......
......@@ -6,11 +6,22 @@
# the codebase) could trigger the need to extract these patterns into a single,
# reusable, sharable helper.
module TrialStatusWidgetHelper
D14_CALLOUT_RANGE = (7..14).freeze # between 14 & 7 days remaining
D3_CALLOUT_RANGE = (0..3).freeze # between 3 & 0 days remaining
# NOTE: We are okay hard-coding the production value for the Ulitmate 1-year
# SaaS plan ID while this is all part of an active experiment. If & when the
# experiment is deemed a success, part of the clean-up effort will be to
# pull the value directly from the CustomersDot API. Value taken from
# https://gitlab.com/gitlab-org/customers-gitlab-com/blob/7177f13c478ef623b779d6635c4a58ee650b7884/config/application.yml#L207
ZUORA_ULTIMATE_PLAN_ID = '2c92a0ff76f0d5250176f2f8c86f305a'
def trial_status_popover_data_attrs(group)
base_attrs = trial_status_common_data_attrs(group)
base_attrs.merge(
group_name: group.name,
purchase_href: ultimate_subscription_path_for_group(group),
start_initially_shown: force_popover_to_be_shown?(group.trial_days_remaining),
target_id: base_attrs[:container_id],
trial_end_date: group.trial_ends_on
)
......@@ -38,6 +49,10 @@ module TrialStatusWidgetHelper
group.trial_active? && can?(current_user, :admin_namespace, group)
end
def force_popover_to_be_shown?(days_remaining)
D14_CALLOUT_RANGE.cover?(days_remaining) || D3_CALLOUT_RANGE.cover?(days_remaining)
end
def trial_status_common_data_attrs(group)
{
container_id: 'trial-status-sidebar-widget',
......@@ -47,13 +62,6 @@ module TrialStatusWidgetHelper
end
def ultimate_subscription_path_for_group(group)
# NOTE: We are okay hard-coding the production value for the Ulitmate 1-year
# SaaS plan ID while this is all part of an active experiment. If & when the
# experiment is deemed a success, part of the clean-up effort will be to
# pull the value directly from the CustomersDot API. Value taken from
# https://gitlab.com/gitlab-org/customers-gitlab-com/blob/7177f13c478ef623b779d6635c4a58ee650b7884/config/application.yml#L207
zuora_ultimate_plan_id = '2c92a0ff76f0d5250176f2f8c86f305a'
new_subscriptions_path(namespace_id: group.id, plan_id: zuora_ultimate_plan_id)
new_subscriptions_path(namespace_id: group.id, plan_id: ZUORA_ULTIMATE_PLAN_ID)
end
end
......@@ -7,6 +7,7 @@ exports[`TrialStatusPopover component matches the snapshot 1`] = `
delay="[object Object]"
placement="rightbottom"
target="target-element-identifier"
triggers="hover focus"
>
<gl-sprintf-stub
......
......@@ -5,6 +5,7 @@ import Vue from 'vue';
import { POPOVER, TRACKING_PROPERTY } from 'ee/contextual_sidebar/components/constants';
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';
Vue.config.ignoredElements = ['gl-emoji'];
......@@ -14,7 +15,6 @@ describe('TrialStatusPopover component', () => {
const { trackingEvents } = POPOVER;
const findByTestId = (testId) => wrapper.find(`[data-testid="${testId}"]`);
const findGlPopover = () => wrapper.findComponent(GlPopover);
const expectTracking = ({ action, ...options } = {}) => {
......@@ -24,17 +24,20 @@ describe('TrialStatusPopover component', () => {
});
};
const createComponent = (mountFn = shallowMount) => {
return mountFn(TrialStatusPopover, {
provide: {
groupName: 'Some Test Group',
planName: 'Ultimate',
plansHref: 'billing/path-for/group',
purchaseHref: 'transactions/new',
targetId: 'target-element-identifier',
trialEndDate: new Date('2021-02-28'),
},
});
const createComponent = (providers = {}, mountFn = shallowMount) => {
return extendedWrapper(
mountFn(TrialStatusPopover, {
provide: {
groupName: 'Some Test Group',
planName: 'Ultimate',
plansHref: 'billing/path-for/group',
purchaseHref: 'transactions/new',
targetId: 'target-element-identifier',
trialEndDate: new Date('2021-02-28'),
...providers,
},
}),
);
};
beforeEach(() => {
......@@ -60,17 +63,84 @@ describe('TrialStatusPopover component', () => {
});
it('tracks when the upgrade button is clicked', () => {
findByTestId('upgradeBtn').vm.$emit('click');
wrapper.findByTestId('upgradeBtn').vm.$emit('click');
expectTracking(trackingEvents.upgradeBtnClick);
});
it('tracks when the compare button is clicked', () => {
findByTestId('compareBtn').vm.$emit('click');
wrapper.findByTestId('compareBtn').vm.$emit('click');
expectTracking(trackingEvents.compareBtnClick);
});
describe('startInitiallyShown', () => {
describe('when set to true', () => {
beforeEach(() => {
wrapper = createComponent({ startInitiallyShown: true });
});
it('causes the popover to be shown by default', () => {
expect(findGlPopover().attributes('show')).toBeTruthy();
});
it('removes the popover triggers', () => {
expect(findGlPopover().attributes('triggers')).toBe('');
});
});
describe('when set to false', () => {
beforeEach(() => {
wrapper = createComponent({ startInitiallyShown: false });
});
it('does not cause the popover to be shown by default', () => {
expect(findGlPopover().attributes('show')).toBeFalsy();
});
it('uses the standard triggers for the popover', () => {
expect(findGlPopover().attributes('triggers')).toBe('hover focus');
});
});
});
describe('close button', () => {
describe('when the popover starts off forcibly shown', () => {
beforeEach(() => {
wrapper = createComponent({ startInitiallyShown: true }, mount);
});
it('is rendered', () => {
expect(wrapper.findByTestId('closeBtn').exists()).toBeTruthy();
});
describe('when clicked', () => {
beforeEach(async () => {
wrapper.findByTestId('closeBtn').trigger('click');
await wrapper.vm.$nextTick();
});
it('closes the popover component', () => {
expect(findGlPopover().props('show')).toBeFalsy();
});
it('tracks an event', () => {
expectTracking(trackingEvents.closeBtnClick);
});
it('continues to be shown in the popover', () => {
expect(wrapper.findByTestId('closeBtn').exists()).toBeTruthy();
});
});
});
describe('when the popover does not start off forcibly shown', () => {
it('is not rendered', () => {
expect(wrapper.findByTestId('closeBtn').exists()).toBeFalsy();
});
});
});
describe('methods', () => {
describe('onResize', () => {
it.each`
......
......@@ -4,6 +4,11 @@ require 'spec_helper'
RSpec.describe TrialStatusWidgetHelper do
describe 'data attributes for mounting Vue components' do
let(:trial_length) { 30 } # days
let(:today_for_specs) { Date.parse('2021-01-15') }
let(:trial_days_remaining) { 18 }
let(:trial_end_date) { Date.current.advance(days: trial_days_remaining) }
let(:trial_percentage_complete) { (trial_length - trial_days_remaining) * 100 / trial_length }
let(:subscription) { instance_double(GitlabSubscription, plan_title: 'Ultimate') }
let(:group) do
......@@ -12,9 +17,9 @@ RSpec.describe TrialStatusWidgetHelper do
name: 'Pants Group',
to_param: 'pants-group',
gitlab_subscription: subscription,
trial_days_remaining: 12,
trial_ends_on: Date.current.advance(days: 18),
trial_percentage_complete: 40
trial_days_remaining: trial_days_remaining,
trial_ends_on: trial_end_date,
trial_percentage_complete: trial_percentage_complete
)
end
......@@ -27,21 +32,70 @@ RSpec.describe TrialStatusWidgetHelper do
end
before do
travel_to Date.parse('2021-01-12')
travel_to today_for_specs
end
describe '#trial_status_popover_data_attrs' do
let(:popover_shared_expected_attrs) do
shared_expected_attrs.merge(
group_name: group.name,
purchase_href: new_subscriptions_path(namespace_id: group.id, plan_id: described_class::ZUORA_ULTIMATE_PLAN_ID),
target_id: shared_expected_attrs[:container_id],
start_initially_shown: false,
trial_end_date: trial_end_date
)
end
subject(:data_attrs) { helper.trial_status_popover_data_attrs(group) }
it 'returns the needed data attributes for mounting the Vue component' do
expect(data_attrs).to match(
shared_expected_attrs.merge(
group_name: 'Pants Group',
purchase_href: '/-/subscriptions/new?namespace_id=123&plan_id=2c92a0ff76f0d5250176f2f8c86f305a',
target_id: shared_expected_attrs[:container_id],
trial_end_date: Date.parse('2021-01-30')
shared_examples 'returned data attributes' do |shown: false|
it 'returns the correct set of data attributes' do
expect(data_attrs).to match(
popover_shared_expected_attrs.merge(
start_initially_shown: shown
)
)
)
end
end
context 'when more than 14 days remain' do
where trial_days_remaining: [15, 22, 30]
with_them do
include_examples 'returned data attributes'
end
end
context 'when between 7 & 14 days remain' do
where trial_days_remaining: [7, 10, 14]
with_them do
include_examples 'returned data attributes', shown: true
end
end
context 'when between 4 & 6 days remain' do
where trial_days_remaining: [4, 5, 6]
with_them do
include_examples 'returned data attributes'
end
end
context 'when between 0 & 3 days remain' do
where trial_days_remaining: [0, 1, 3]
with_them do
include_examples 'returned data attributes', shown: true
end
end
context 'when fewer than 0 days remain' do
where trial_days_remaining: [-1, -5, -12]
with_them do
include_examples 'returned data attributes'
end
end
end
......@@ -55,9 +109,9 @@ RSpec.describe TrialStatusWidgetHelper do
it 'returns the needed data attributes for mounting the Vue component' do
expect(data_attrs).to match(
shared_expected_attrs.merge(
days_remaining: 12,
days_remaining: trial_days_remaining,
nav_icon_image_path: '/image-path/for-file.svg',
percentage_complete: 40
percentage_complete: trial_percentage_complete
)
)
end
......
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