Commit 02321dcf authored by Angelo Gulina's avatar Angelo Gulina Committed by Vitaly Slobodin

Re-use common logic for addons purchase

parent 831b8720
<script>
import emptySvg from '@gitlab/svgs/dist/illustrations/security-dashboard-empty-state.svg';
import { GlEmptyState, GlIcon, GlTooltipDirective } from '@gitlab/ui';
import * as Sentry from '@sentry/browser';
import OrderSummary from 'ee/subscriptions/buy_addons_shared/components/order_summary.vue';
import { ERROR_FETCHING_DATA_HEADER, ERROR_FETCHING_DATA_DESCRIPTION } from '~/ensure_data';
import Checkout from 'ee/subscriptions/buy_addons_shared/components/checkout.vue';
import AddonPurchaseDetails from 'ee/subscriptions/buy_addons_shared/components/checkout/addon_purchase_details.vue';
import { formatNumber, sprintf } from '~/locale';
import { CUSTOMERSDOT_CLIENT } from 'ee/subscriptions/buy_addons_shared/constants';
import plansQuery from 'ee/subscriptions/graphql/queries/plans.customer.query.graphql';
import stateQuery from 'ee/subscriptions/graphql/queries/state.query.graphql';
export default {
components: {
AddonPurchaseDetails,
Checkout,
GlEmptyState,
GlIcon,
OrderSummary,
},
directives: {
GlTooltip: GlTooltipDirective,
},
props: {
config: {
required: true,
type: Object,
},
tags: {
required: true,
type: Array,
},
},
data() {
return {
hasError: false,
};
},
computed: {
emptySvgPath() {
return `data:image/svg+xml;utf8,${encodeURIComponent(emptySvg)}`;
},
errorDescription() {
return ERROR_FETCHING_DATA_DESCRIPTION;
},
errorTitle() {
return ERROR_FETCHING_DATA_HEADER;
},
isQuantityValid() {
return Number.isFinite(this.quantity) && this.quantity > 0;
},
formulaText() {
const formulaText = this.isQuantityValid ? this.config.formula : this.config.formulaWithAlert;
return sprintf(formulaText, {
quantity: formatNumber(this.config.quantityPerPack),
units: this.config.productUnit,
});
},
formulaTotal() {
const total = sprintf(this.config.formulaTotal, {
quantity: formatNumber(this.totalUnits),
});
return this.isQuantityValid ? total : '';
},
plan() {
return {
...this.plans[0],
isAddon: true,
};
},
slotProps() {
return this.plan;
},
totalUnits() {
return this.quantity * this.config.quantityPerPack;
},
summaryTitle() {
return sprintf(this.config.summaryTitle(this.quantity), { quantity: this.quantity });
},
summaryTotal() {
return sprintf(this.config.summaryTotal, {
quantity: formatNumber(this.totalUnits),
});
},
},
methods: {
pricePerUnitLabel(price) {
return sprintf(this.config.pricePerUnit, {
selectedPlanPrice: price,
});
},
},
apollo: {
plans: {
client: CUSTOMERSDOT_CLIENT,
query: plansQuery,
variables() {
return { tags: this.tags };
},
update(data) {
if (!data?.plans?.length) {
this.hasError = true;
return [];
}
return data.plans;
},
error(error) {
this.hasError = true;
Sentry.captureException(error);
},
},
quantity: {
query: stateQuery,
update(data) {
return data.subscription.quantity;
},
},
},
};
</script>
<template>
<gl-empty-state
v-if="hasError"
:description="errorDescription"
:title="errorTitle"
:svg-path="emptySvgPath"
/>
<div
v-else-if="!$apollo.loading"
data-testid="buy-addons-shared"
class="row gl-flex-grow-1 gl-flex-direction-column gl-flex-nowrap gl-lg-flex-direction-row gl-xl-flex-direction-row gl-lg-flex-wrap gl-xl-flex-wrap"
>
<div
class="checkout-pane gl-px-3 gl-align-items-center gl-bg-gray-10 col-lg-7 gl-display-flex gl-flex-direction-column gl-flex-grow-1"
>
<checkout :plan="plan">
<template #purchase-details>
<addon-purchase-details
:product-label="config.productLabel"
:quantity="quantity"
:show-alert="true"
:alert-text="config.alertText"
>
<template #formula>
{{ formulaText }}
<strong>{{ formulaTotal }}</strong>
</template>
<template #summary-label>
<strong data-testid="summary-label">
{{ summaryTitle }}
</strong>
<div data-testid="summary-total">{{ summaryTotal }}</div>
</template>
</addon-purchase-details>
</template>
</checkout>
</div>
<div
class="gl-pb-3 gl-px-3 gl-lg-px-7 col-lg-5 gl-display-flex gl-flex-direction-row gl-justify-content-center"
>
<order-summary
:plan="plan"
:title="config.title"
:purchase-has-expiration="config.hasExpiration"
>
<template #price-per-unit="{ price }">
{{ pricePerUnitLabel(price) }}
</template>
<template #tooltip>
<gl-icon
v-gl-tooltip.right
:title="config.tooltipNote"
:aria-label="config.tooltipNote"
role="tooltip"
name="question"
/>
</template>
</order-summary>
</div>
</div>
</template>
......@@ -2,7 +2,6 @@
import { GlAlert, GlFormInput } from '@gitlab/ui';
import { STEPS } from 'ee/subscriptions/constants';
import updateState from 'ee/subscriptions/graphql/mutations/update_state.mutation.graphql';
import stateQuery from 'ee/subscriptions/graphql/queries/state.query.graphql';
import Step from 'ee/vue_shared/purchase_flow/components/step.vue';
import { GENERAL_ERROR_MESSAGE } from 'ee/vue_shared/purchase_flow/constants';
import createFlash from '~/flash';
......@@ -28,7 +27,7 @@ export default {
type: String,
required: true,
},
quantityPerPack: {
quantity: {
type: Number,
required: true,
},
......@@ -42,14 +41,6 @@ export default {
default: '',
},
},
apollo: {
quantity: {
query: stateQuery,
update(data) {
return data.subscription.quantity;
},
},
},
computed: {
quantityModel: {
get() {
......@@ -62,9 +53,6 @@ export default {
isValid() {
return this.quantity > 0;
},
totalUnits() {
return this.quantity * this.quantityPerPack;
},
},
methods: {
updateQuantity(quantity = 0) {
......@@ -121,12 +109,12 @@ export default {
class="gl-w-15"
/>
<div class="gl-ml-3" data-testid="addon-quantity-text">
<slot name="formula" :quantity="totalUnits"></slot>
<slot name="formula"></slot>
</div>
</div>
</template>
<template #summary>
<slot name="summary-label" :quantity="quantity"></slot>
<slot name="summary-label"></slot>
</template>
</step>
</template>
......@@ -22,7 +22,7 @@ export const STORAGE_PER_PACK = 10;
export const I18N_CI_MINUTES_PRODUCT_LABEL = s__('Checkout|CI minute pack');
export const I18N_CI_MINUTES_PRODUCT_UNIT = s__('Checkout|minutes');
export const I18N_CI_MINUTES_FORMULA_TOTAL = s__('Checkout|%{totalCiMinutes} CI minutes');
export const I18N_CI_MINUTES_FORMULA_TOTAL = s__('Checkout|%{quantity} CI minutes');
export const i18nCIMinutesSummaryTitle = (quantity) =>
n__('Checkout|%d CI minute pack', 'Checkout|%d CI minute packs', quantity);
export const I18N_CI_MINUTES_SUMMARY_TOTAL = s__('Checkout|Total minutes: %{quantity}');
......
<script>
import emptySvg from '@gitlab/svgs/dist/illustrations/security-dashboard-empty-state.svg';
import { GlEmptyState } from '@gitlab/ui';
import * as Sentry from '@sentry/browser';
import StepOrderApp from 'ee/vue_shared/purchase_flow/components/step_order_app.vue';
import { ERROR_FETCHING_DATA_HEADER, ERROR_FETCHING_DATA_DESCRIPTION } from '~/ensure_data';
import { sprintf, formatNumber } from '~/locale';
import Checkout from '../../buy_addons_shared/components/checkout.vue';
import AddonPurchaseDetails from '../../buy_addons_shared/components/checkout/addon_purchase_details.vue';
import OrderSummary from '../../buy_addons_shared/components/order_summary.vue';
import BuyAddonsApp from 'ee/subscriptions/buy_addons_shared/components/app.vue';
import {
CI_MINUTES_PER_PACK,
planTags,
I18N_CI_MINUTES_PRICE_PRE_UNIT,
I18N_CI_MINUTES_PRODUCT_LABEL,
I18N_CI_MINUTES_PRODUCT_UNIT,
I18N_DETAILS_FORMULA,
......@@ -17,146 +12,38 @@ import {
i18nCIMinutesSummaryTitle,
I18N_CI_MINUTES_SUMMARY_TOTAL,
I18N_CI_MINUTES_ALERT_TEXT,
I18N_CI_MINUTES_PRICE_PRE_UNIT,
I18N_CI_MINUTES_TITLE,
planTags,
CUSTOMERSDOT_CLIENT,
CI_MINUTES_PER_PACK,
} from '../../buy_addons_shared/constants';
import plansQuery from '../../graphql/queries/plans.customer.query.graphql';
} from 'ee/subscriptions/buy_addons_shared/constants';
export default {
components: {
Checkout,
GlEmptyState,
OrderSummary,
StepOrderApp,
AddonPurchaseDetails,
},
i18n: {
ERROR_FETCHING_DATA_HEADER,
ERROR_FETCHING_DATA_DESCRIPTION,
productLabel: I18N_CI_MINUTES_PRODUCT_LABEL,
productUnit: I18N_CI_MINUTES_PRODUCT_UNIT,
formula: I18N_DETAILS_FORMULA,
formulaWithAlert: I18N_DETAILS_FORMULA_WITH_ALERT,
formulaTotal: I18N_CI_MINUTES_FORMULA_TOTAL,
summaryTitle: i18nCIMinutesSummaryTitle,
summaryTotal: I18N_CI_MINUTES_SUMMARY_TOTAL,
alertText: I18N_CI_MINUTES_ALERT_TEXT,
title: I18N_CI_MINUTES_TITLE,
pricePerUnit: I18N_CI_MINUTES_PRICE_PRE_UNIT,
},
CI_MINUTES_PER_PACK,
emptySvg,
data() {
return {
hasError: false,
};
BuyAddonsApp,
},
computed: {
plan() {
tags() {
return [planTags.CI_1000_MINUTES_PLAN];
},
config() {
// These will move into the GraphQL store. See: https://gitlab.com/gitlab-org/gitlab/-/issues/346620
return {
...this.plans[0],
isAddon: true,
alertText: I18N_CI_MINUTES_ALERT_TEXT,
formula: I18N_DETAILS_FORMULA,
formulaWithAlert: I18N_DETAILS_FORMULA_WITH_ALERT,
formulaTotal: I18N_CI_MINUTES_FORMULA_TOTAL,
hasExpiration: false,
pricePerUnit: I18N_CI_MINUTES_PRICE_PRE_UNIT,
productLabel: I18N_CI_MINUTES_PRODUCT_LABEL,
productUnit: I18N_CI_MINUTES_PRODUCT_UNIT,
quantityPerPack: CI_MINUTES_PER_PACK,
summaryTitle: i18nCIMinutesSummaryTitle,
summaryTotal: I18N_CI_MINUTES_SUMMARY_TOTAL,
title: I18N_CI_MINUTES_TITLE,
tooltipNote: '',
};
},
},
methods: {
isQuantityValid(quantity) {
return Number.isFinite(quantity) && quantity > 0;
},
formulaText(quantity) {
const formulaText = this.isQuantityValid(quantity)
? this.$options.i18n.formula
: this.$options.i18n.formulaWithAlert;
return sprintf(formulaText, {
quantity: formatNumber(CI_MINUTES_PER_PACK),
units: this.$options.i18n.productUnit,
});
},
formulaTotal(quantity) {
const total = sprintf(this.$options.i18n.formulaTotal, {
totalCiMinutes: formatNumber(quantity),
});
return this.isQuantityValid(quantity) ? total : '';
},
summaryTitle(quantity) {
return sprintf(this.$options.i18n.summaryTitle(quantity), { quantity });
},
summaryTotal(quantity) {
return sprintf(this.$options.i18n.summaryTotal, {
quantity: formatNumber(quantity * CI_MINUTES_PER_PACK),
});
},
pricePerUnitLabel(price) {
return sprintf(this.$options.i18n.pricePerUnit, {
selectedPlanPrice: price,
});
},
},
apollo: {
plans: {
client: CUSTOMERSDOT_CLIENT,
query: plansQuery,
variables: {
tags: [planTags.CI_1000_MINUTES_PLAN],
},
update(data) {
if (!data?.plans?.length) {
this.hasError = true;
return null;
}
return data.plans;
},
error(error) {
this.hasError = true;
Sentry.captureException(error);
},
},
},
};
</script>
<template>
<gl-empty-state
v-if="hasError"
:title="$options.i18n.ERROR_FETCHING_DATA_HEADER"
:description="$options.i18n.ERROR_FETCHING_DATA_DESCRIPTION"
:svg-path="`data:image/svg+xml;utf8,${encodeURIComponent($options.emptySvg)}`"
/>
<step-order-app v-else-if="!$apollo.loading">
<template #checkout>
<checkout :plan="plan">
<template #purchase-details>
<addon-purchase-details
:product-label="$options.i18n.productLabel"
:quantity-per-pack="$options.CI_MINUTES_PER_PACK"
:show-alert="true"
:alert-text="$options.i18n.alertText"
>
<template #formula="{ quantity }">
{{ formulaText(quantity) }}
<strong>{{ formulaTotal(quantity) }}</strong>
</template>
<template #summary-label="{ quantity }">
<strong data-testid="summary-label">
{{ summaryTitle(quantity) }}
</strong>
<div data-testid="summary-total">{{ summaryTotal(quantity) }}</div>
</template>
</addon-purchase-details>
</template>
</checkout>
</template>
<template #order-summary>
<order-summary :plan="plan" :title="$options.i18n.title">
<template #price-per-unit="{ price }">
{{ pricePerUnitLabel(price) }}
</template>
</order-summary>
</template>
</step-order-app>
<buy-addons-app :config="config" :tags="tags" />
</template>
<script>
import emptySvg from '@gitlab/svgs/dist/illustrations/security-dashboard-empty-state.svg';
import { GlEmptyState, GlIcon, GlTooltipDirective } from '@gitlab/ui';
import * as Sentry from '@sentry/browser';
import StepOrderApp from 'ee/vue_shared/purchase_flow/components/step_order_app.vue';
import { ERROR_FETCHING_DATA_HEADER, ERROR_FETCHING_DATA_DESCRIPTION } from '~/ensure_data';
import { sprintf, formatNumber } from '~/locale';
import Checkout from '../../buy_addons_shared/components/checkout.vue';
import AddonPurchaseDetails from '../../buy_addons_shared/components/checkout/addon_purchase_details.vue';
import OrderSummary from '../../buy_addons_shared/components/order_summary.vue';
import BuyAddonsApp from 'ee/subscriptions/buy_addons_shared/components/app.vue';
import {
I18N_STORAGE_PRODUCT_LABEL,
I18N_STORAGE_PRODUCT_UNIT,
......@@ -20,153 +12,39 @@ import {
I18N_STORAGE_PRICE_PRE_UNIT,
I18N_STORAGE_TOOLTIP_NOTE,
planTags,
CUSTOMERSDOT_CLIENT,
STORAGE_PER_PACK,
} from '../../buy_addons_shared/constants';
import plansQuery from '../../graphql/queries/plans.customer.query.graphql';
} from 'ee/subscriptions/buy_addons_shared/constants';
export default {
name: 'BuyStorageApp',
components: {
Checkout,
GlEmptyState,
OrderSummary,
StepOrderApp,
AddonPurchaseDetails,
GlIcon,
},
directives: {
GlTooltip: GlTooltipDirective,
},
i18n: {
ERROR_FETCHING_DATA_HEADER,
ERROR_FETCHING_DATA_DESCRIPTION,
productLabel: I18N_STORAGE_PRODUCT_LABEL,
productUnit: I18N_STORAGE_PRODUCT_UNIT,
formula: I18N_DETAILS_FORMULA,
formulaWithAlert: I18N_DETAILS_FORMULA_WITH_ALERT,
formulaTotal: I18N_STORAGE_FORMULA_TOTAL,
summaryTitle: i18nStorageSummaryTitle,
summaryTotal: I18N_STORAGE_SUMMARY_TOTAL,
title: I18N_STORAGE_TITLE,
pricePerUnit: I18N_STORAGE_PRICE_PRE_UNIT,
tooltipNote: I18N_STORAGE_TOOLTIP_NOTE,
},
emptySvg,
STORAGE_PER_PACK,
data() {
return {
hasError: false,
};
BuyAddonsApp,
},
computed: {
plan() {
tags() {
return [planTags.STORAGE_PLAN];
},
config() {
// These will move into the GraphQL store. See: https://gitlab.com/gitlab-org/gitlab/-/issues/346620
return {
...this.plans[0],
isAddon: true,
alertText: '',
formula: I18N_DETAILS_FORMULA,
formulaWithAlert: I18N_DETAILS_FORMULA_WITH_ALERT,
formulaTotal: I18N_STORAGE_FORMULA_TOTAL,
hasExpiration: true,
pricePerUnit: I18N_STORAGE_PRICE_PRE_UNIT,
productLabel: I18N_STORAGE_PRODUCT_LABEL,
productUnit: I18N_STORAGE_PRODUCT_UNIT,
quantityPerPack: STORAGE_PER_PACK,
summaryTitle: i18nStorageSummaryTitle,
summaryTotal: I18N_STORAGE_SUMMARY_TOTAL,
title: I18N_STORAGE_TITLE,
tooltipNote: I18N_STORAGE_TOOLTIP_NOTE,
};
},
},
methods: {
isQuantityValid(quantity) {
return Number.isFinite(quantity) && quantity > 0;
},
formulaText(quantity) {
const formulaText = this.isQuantityValid(quantity)
? this.$options.i18n.formula
: this.$options.i18n.formulaWithAlert;
return sprintf(formulaText, {
quantity: formatNumber(STORAGE_PER_PACK),
units: this.$options.i18n.productUnit,
});
},
formulaTotal(quantity) {
const total = sprintf(this.$options.i18n.formulaTotal, { quantity: formatNumber(quantity) });
return this.isQuantityValid(quantity) ? total : '';
},
summaryTitle(quantity) {
return sprintf(this.$options.i18n.summaryTitle(quantity), { quantity });
},
summaryTotal(quantity) {
return sprintf(this.$options.i18n.summaryTotal, {
quantity: formatNumber(quantity * STORAGE_PER_PACK),
});
},
pricePerUnitLabel(price) {
return sprintf(this.$options.i18n.pricePerUnit, {
selectedPlanPrice: price,
});
},
},
apollo: {
plans: {
client: CUSTOMERSDOT_CLIENT,
query: plansQuery,
variables: {
tags: [planTags.STORAGE_PLAN],
},
update(data) {
if (!data?.plans?.length) {
this.hasError = true;
return null;
}
return data.plans;
},
error(error) {
this.hasError = true;
Sentry.captureException(error);
},
},
},
};
</script>
<template>
<gl-empty-state
v-if="hasError"
:title="$options.i18n.ERROR_FETCHING_DATA_HEADER"
:description="$options.i18n.ERROR_FETCHING_DATA_DESCRIPTION"
:svg-path="`data:image/svg+xml;utf8,${encodeURIComponent($options.emptySvg)}`"
/>
<step-order-app v-else-if="!$apollo.loading">
<template #checkout>
<checkout :plan="plan">
<template #purchase-details>
<addon-purchase-details
:product-label="$options.i18n.productLabel"
:quantity-per-pack="$options.STORAGE_PER_PACK"
>
<template #formula="{ quantity }">
{{ formulaText(quantity) }}
<strong>{{ formulaTotal(quantity) }}</strong>
</template>
<template #summary-label="{ quantity }">
<strong data-testid="summary-label">
{{ summaryTitle(quantity) }}
</strong>
<p class="gl-mb-0" data-testid="summary-total">{{ summaryTotal(quantity) }}</p>
</template>
</addon-purchase-details>
</template>
</checkout>
</template>
<template #order-summary>
<order-summary :plan="plan" :title="$options.i18n.title" purchase-has-expiration>
<template #price-per-unit="{ price }">
{{ pricePerUnitLabel(price) }}
</template>
<template #tooltip>
<gl-icon
v-gl-tooltip.right
:title="$options.i18n.tooltipNote"
:aria-label="$options.i18n.tooltipNote"
role="tooltip"
name="question"
/>
</template>
</order-summary>
</template>
</step-order-app>
<buy-addons-app :config="config" :tags="tags" />
</template>
import { GlEmptyState } from '@gitlab/ui';
import { createLocalVue } from '@vue/test-utils';
import VueApollo from 'vue-apollo';
import { pick } from 'lodash';
import {
I18N_STORAGE_PRODUCT_LABEL,
I18N_STORAGE_PRODUCT_UNIT,
I18N_DETAILS_FORMULA,
I18N_STORAGE_FORMULA_TOTAL,
I18N_DETAILS_FORMULA_WITH_ALERT,
i18nStorageSummaryTitle,
I18N_STORAGE_SUMMARY_TOTAL,
I18N_STORAGE_TITLE,
I18N_STORAGE_PRICE_PRE_UNIT,
I18N_STORAGE_TOOLTIP_NOTE,
planTags,
STORAGE_PER_PACK,
} from 'ee/subscriptions/buy_addons_shared/constants';
import Checkout from 'ee/subscriptions/buy_addons_shared/components/checkout.vue';
import AddonPurchaseDetails from 'ee/subscriptions/buy_addons_shared/components/checkout/addon_purchase_details.vue';
import OrderSummary from 'ee/subscriptions/buy_addons_shared/components/order_summary.vue';
import SummaryDetails from 'ee/subscriptions/buy_addons_shared/components/order_summary/summary_details.vue';
import App from 'ee/subscriptions/buy_addons_shared/components/app.vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { createMockApolloProvider } from 'ee_jest/subscriptions/spec_helper';
import { mockStoragePlans } from 'ee_jest/subscriptions/mock_data';
const localVue = createLocalVue();
localVue.use(VueApollo);
describe('Buy Storage App', () => {
let wrapper;
function createComponent(apolloProvider) {
wrapper = shallowMountExtended(App, {
localVue,
apolloProvider,
propsData: {
config: {
alertText: '',
formula: I18N_DETAILS_FORMULA,
formulaWithAlert: I18N_DETAILS_FORMULA_WITH_ALERT,
formulaTotal: I18N_STORAGE_FORMULA_TOTAL,
hasExpiration: true,
pricePerUnit: I18N_STORAGE_PRICE_PRE_UNIT,
productLabel: I18N_STORAGE_PRODUCT_LABEL,
productUnit: I18N_STORAGE_PRODUCT_UNIT,
quantityPerPack: STORAGE_PER_PACK,
summaryTitle: i18nStorageSummaryTitle,
summaryTotal: I18N_STORAGE_SUMMARY_TOTAL,
title: I18N_STORAGE_TITLE,
tooltipNote: I18N_STORAGE_TOOLTIP_NOTE,
},
tags: [planTags.STORAGE_PLAN],
},
stubs: {
Checkout,
AddonPurchaseDetails,
OrderSummary,
SummaryDetails,
},
});
return waitForPromises();
}
const getStoragePlan = () => pick(mockStoragePlans[0], ['id', 'code', 'pricePerYear', 'name']);
const findCheckout = () => wrapper.findComponent(Checkout);
const findOrderSummary = () => wrapper.findComponent(OrderSummary);
const findPriceLabel = () => wrapper.findByTestId('price-per-unit');
const findQuantityText = () => wrapper.findByTestId('addon-quantity-text');
const findRootElement = () => wrapper.findByTestId('buy-addons-shared');
const findSummaryLabel = () => wrapper.findByTestId('summary-label');
const findSummaryTotal = () => wrapper.findByTestId('summary-total');
afterEach(() => {
wrapper.destroy();
});
describe('when data is received', () => {
beforeEach(() => {
const mockApollo = createMockApolloProvider();
return createComponent(mockApollo);
});
it('should display the root element', () => {
expect(findRootElement().exists()).toBe(true);
expect(wrapper.findComponent(GlEmptyState).exists()).toBe(false);
});
it('provides the correct props to checkout', () => {
expect(findCheckout().props()).toMatchObject({
plan: { ...getStoragePlan, isAddon: true },
});
});
it('provides the correct props to order summary', () => {
expect(findOrderSummary().props()).toMatchObject({
plan: { ...getStoragePlan, isAddon: true },
title: I18N_STORAGE_TITLE,
});
});
});
describe('when data is not received', () => {
it('should display the GlEmptyState for empty data', async () => {
const mockApollo = createMockApolloProvider({
plansQueryMock: jest.fn().mockResolvedValue({ data: null }),
});
await createComponent(mockApollo);
expect(findRootElement().exists()).toBe(false);
expect(wrapper.findComponent(GlEmptyState).exists()).toBe(true);
});
it('should display the GlEmptyState for empty plans', async () => {
const mockApollo = createMockApolloProvider({
plansQueryMock: jest.fn().mockResolvedValue({ data: { plans: null } }),
});
await createComponent(mockApollo);
expect(findRootElement().exists()).toBe(false);
expect(wrapper.findComponent(GlEmptyState).exists()).toBe(true);
});
it('should display the GlEmptyState for plans data of wrong type', async () => {
const mockApollo = createMockApolloProvider({
plansQueryMock: jest.fn().mockResolvedValue({ data: { plans: {} } }),
});
await createComponent(mockApollo);
expect(findRootElement().exists()).toBe(false);
expect(wrapper.findComponent(GlEmptyState).exists()).toBe(true);
});
});
describe('when an error is received', () => {
it('should display the GlEmptyState', async () => {
const mockApollo = createMockApolloProvider({
plansQueryMock: jest.fn().mockRejectedValue(new Error('An error happened!')),
});
await createComponent(mockApollo);
expect(findRootElement().exists()).toBe(false);
expect(wrapper.findComponent(GlEmptyState).exists()).toBe(true);
});
});
describe('labels', () => {
it('shows labels correctly for 1 pack', async () => {
const mockApollo = createMockApolloProvider();
await createComponent(mockApollo);
expect(findQuantityText().text()).toMatchInterpolatedText(
'x 10 GB per pack = 10 GB of storage',
);
expect(findSummaryLabel().text()).toBe('1 storage pack');
expect(findSummaryTotal().text()).toBe('Total storage: 10 GB');
expect(findPriceLabel().text()).toBe('$10 per 10 GB storage per pack');
});
it('shows labels correctly for 2 packs', async () => {
const mockApollo = createMockApolloProvider({}, { quantity: 2 });
await createComponent(mockApollo);
expect(findQuantityText().text()).toMatchInterpolatedText(
'x 10 GB per pack = 20 GB of storage',
);
expect(findSummaryLabel().text()).toBe('2 storage packs');
expect(findSummaryTotal().text()).toBe('Total storage: 20 GB');
});
it('does not show labels if input is invalid', async () => {
const mockApollo = createMockApolloProvider({}, { quantity: -1 });
await createComponent(mockApollo);
expect(findQuantityText().text()).toMatchInterpolatedText('x 10 GB per pack');
});
});
});
......@@ -38,7 +38,7 @@ describe('AddonPurchaseDetails', () => {
},
propsData: {
productLabel: 'CI minute pack',
quantityPerPack: 1000,
quantity: 10,
packsFormula: 'x %{packQuantity} minutes per pack = %{strong}',
quantityText: '%{quantity} CI minutes',
totalPurchase: 'Total minutes: %{quantity}',
......@@ -73,9 +73,12 @@ describe('AddonPurchaseDetails', () => {
});
it('is invalid when quantity is less than 1', async () => {
createComponent({
subscription: { namespaceId: 483, quantity: 0 },
});
createComponent(
{
subscription: { namespaceId: 483 },
},
{ quantity: 0 },
);
expect(isStepValid()).toBe(false);
});
......
import { GlEmptyState } from '@gitlab/ui';
import { createLocalVue } from '@vue/test-utils';
import VueApollo from 'vue-apollo';
import { pick } from 'lodash';
import { I18N_CI_MINUTES_TITLE } from 'ee/subscriptions/buy_addons_shared/constants';
import Checkout from 'ee/subscriptions/buy_addons_shared/components/checkout.vue';
import AddonPurchaseDetails from 'ee/subscriptions/buy_addons_shared/components/checkout/addon_purchase_details.vue';
import OrderSummary from 'ee/subscriptions/buy_addons_shared/components/order_summary.vue';
import SummaryDetails from 'ee/subscriptions/buy_addons_shared/components/order_summary/summary_details.vue';
import App from 'ee/subscriptions/buy_minutes/components/app.vue';
import StepOrderApp from 'ee/vue_shared/purchase_flow/components/step_order_app.vue';
import BuyAddonsApp from 'ee/subscriptions/buy_addons_shared/components/app.vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { createMockApolloProvider } from 'ee_jest/subscriptions/spec_helper';
import { mockCiMinutesPlans } from 'ee_jest/subscriptions/mock_data';
const localVue = createLocalVue();
localVue.use(VueApollo);
import App from 'ee/subscriptions/buy_minutes/components/app.vue';
import {
CI_MINUTES_PER_PACK,
planTags,
I18N_CI_MINUTES_PRICE_PRE_UNIT,
I18N_CI_MINUTES_PRODUCT_LABEL,
I18N_CI_MINUTES_PRODUCT_UNIT,
I18N_DETAILS_FORMULA,
I18N_DETAILS_FORMULA_WITH_ALERT,
I18N_CI_MINUTES_FORMULA_TOTAL,
i18nCIMinutesSummaryTitle,
I18N_CI_MINUTES_SUMMARY_TOTAL,
I18N_CI_MINUTES_ALERT_TEXT,
I18N_CI_MINUTES_TITLE,
} from 'ee/subscriptions/buy_addons_shared/constants';
describe('Buy Minutes App', () => {
let wrapper;
function createComponent(apolloProvider) {
return shallowMountExtended(App, {
localVue,
apolloProvider,
stubs: {
Checkout,
AddonPurchaseDetails,
OrderSummary,
SummaryDetails,
},
});
}
const createComponent = () => {
wrapper = shallowMountExtended(App);
};
const getCiMinutePlan = () => pick(mockCiMinutesPlans[0], ['id', 'code', 'pricePerYear', 'name']);
const findCheckout = () => wrapper.findComponent(Checkout);
const findOrderSummary = () => wrapper.findComponent(OrderSummary);
const findPriceLabel = () => wrapper.findByTestId('price-per-unit');
const findQuantityText = () => wrapper.findByTestId('addon-quantity-text');
const findSummaryLabel = () => wrapper.findByTestId('summary-label');
const findSummaryTotal = () => wrapper.findByTestId('summary-total');
const findBuyAddonsApp = () => wrapper.findComponent(BuyAddonsApp);
afterEach(() => {
wrapper.destroy();
beforeEach(() => {
createComponent();
});
describe('when data is received', () => {
beforeEach(() => {
const mockApollo = createMockApolloProvider();
wrapper = createComponent(mockApollo);
return waitForPromises();
});
it('should display the StepOrderApp', () => {
expect(wrapper.findComponent(StepOrderApp).exists()).toBe(true);
expect(wrapper.findComponent(GlEmptyState).exists()).toBe(false);
});
it('provides the correct props to checkout', () => {
expect(findCheckout().props()).toMatchObject({
plan: { ...getCiMinutePlan, isAddon: true },
});
});
it('provides the correct props to order summary', () => {
expect(findOrderSummary().props()).toMatchObject({
plan: { ...getCiMinutePlan, isAddon: true },
title: I18N_CI_MINUTES_TITLE,
});
});
});
describe('when data is not received', () => {
it('should display the GlEmptyState for empty data', async () => {
const mockApollo = createMockApolloProvider({
plansQueryMock: jest.fn().mockResolvedValue({ data: null }),
});
wrapper = createComponent(mockApollo);
await waitForPromises();
expect(wrapper.findComponent(StepOrderApp).exists()).toBe(false);
expect(wrapper.findComponent(GlEmptyState).exists()).toBe(true);
});
it('should display the GlEmptyState for empty plans', async () => {
const mockApollo = createMockApolloProvider({
plansQueryMock: jest.fn().mockResolvedValue({ data: { plans: null } }),
});
wrapper = createComponent(mockApollo);
await waitForPromises();
expect(wrapper.findComponent(StepOrderApp).exists()).toBe(false);
expect(wrapper.findComponent(GlEmptyState).exists()).toBe(true);
});
it('should display the GlEmptyState for plans data of wrong type', async () => {
const mockApollo = createMockApolloProvider({
plansQueryMock: jest.fn().mockResolvedValue({ data: { plans: {} } }),
});
wrapper = createComponent(mockApollo);
await waitForPromises();
expect(wrapper.findComponent(StepOrderApp).exists()).toBe(false);
expect(wrapper.findComponent(GlEmptyState).exists()).toBe(true);
});
afterEach(() => {
wrapper.destroy();
});
describe('when an error is received', () => {
it('should display the GlEmptyState', async () => {
const mockApollo = createMockApolloProvider({
plansQueryMock: jest.fn().mockRejectedValue(new Error('An error happened!')),
});
wrapper = createComponent(mockApollo);
await waitForPromises();
expect(wrapper.findComponent(StepOrderApp).exists()).toBe(false);
expect(wrapper.findComponent(GlEmptyState).exists()).toBe(true);
});
it('passes the correct tags', () => {
expect(findBuyAddonsApp().props('tags')).toEqual([planTags.CI_1000_MINUTES_PLAN]);
});
describe('labels', () => {
it('shows labels correctly for 1 pack', async () => {
const mockApollo = createMockApolloProvider();
wrapper = createComponent(mockApollo);
await waitForPromises();
expect(findQuantityText().text()).toMatchInterpolatedText(
'x 1,000 minutes per pack = 1,000 CI minutes',
);
expect(findSummaryLabel().text()).toBe('1 CI minute pack');
expect(findSummaryTotal().text()).toBe('Total minutes: 1,000');
expect(findPriceLabel().text()).toBe('$10 per pack of 1,000 minutes');
});
it('shows labels correctly for 2 packs', async () => {
const mockApollo = createMockApolloProvider({}, { quantity: 2 });
wrapper = createComponent(mockApollo);
await waitForPromises();
expect(findQuantityText().text()).toMatchInterpolatedText(
'x 1,000 minutes per pack = 2,000 CI minutes',
);
expect(findSummaryLabel().text()).toBe('2 CI minute packs');
expect(findSummaryTotal().text()).toBe('Total minutes: 2,000');
});
it('does not show labels if input is invalid', async () => {
const mockApollo = createMockApolloProvider({}, { quantity: -1 });
wrapper = createComponent(mockApollo);
await waitForPromises();
expect(findQuantityText().text()).toMatchInterpolatedText('x 1,000 minutes per pack');
it('passes the correct config', () => {
expect(findBuyAddonsApp().props('config')).toMatchObject({
alertText: I18N_CI_MINUTES_ALERT_TEXT,
formula: I18N_DETAILS_FORMULA,
formulaWithAlert: I18N_DETAILS_FORMULA_WITH_ALERT,
formulaTotal: I18N_CI_MINUTES_FORMULA_TOTAL,
hasExpiration: false,
pricePerUnit: I18N_CI_MINUTES_PRICE_PRE_UNIT,
productLabel: I18N_CI_MINUTES_PRODUCT_LABEL,
productUnit: I18N_CI_MINUTES_PRODUCT_UNIT,
quantityPerPack: CI_MINUTES_PER_PACK,
summaryTitle: i18nCIMinutesSummaryTitle,
summaryTotal: I18N_CI_MINUTES_SUMMARY_TOTAL,
title: I18N_CI_MINUTES_TITLE,
tooltipNote: '',
});
});
});
import { GlEmptyState } from '@gitlab/ui';
import { createLocalVue } from '@vue/test-utils';
import VueApollo from 'vue-apollo';
import { pick } from 'lodash';
import { I18N_STORAGE_TITLE } from 'ee/subscriptions/buy_addons_shared/constants';
import Checkout from 'ee/subscriptions/buy_addons_shared/components/checkout.vue';
import AddonPurchaseDetails from 'ee/subscriptions/buy_addons_shared/components/checkout/addon_purchase_details.vue';
import OrderSummary from 'ee/subscriptions/buy_addons_shared/components/order_summary.vue';
import SummaryDetails from 'ee/subscriptions/buy_addons_shared/components/order_summary/summary_details.vue';
import App from 'ee/subscriptions/buy_storage/components/app.vue';
import StepOrderApp from 'ee/vue_shared/purchase_flow/components/step_order_app.vue';
import BuyAddonsApp from 'ee/subscriptions/buy_addons_shared/components/app.vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { createMockApolloProvider } from 'ee_jest/subscriptions/spec_helper';
import { mockStoragePlans } from 'ee_jest/subscriptions/mock_data';
const localVue = createLocalVue();
localVue.use(VueApollo);
import App from 'ee/subscriptions/buy_storage/components/app.vue';
import {
I18N_STORAGE_PRODUCT_LABEL,
I18N_STORAGE_PRODUCT_UNIT,
I18N_DETAILS_FORMULA,
I18N_STORAGE_FORMULA_TOTAL,
I18N_DETAILS_FORMULA_WITH_ALERT,
i18nStorageSummaryTitle,
I18N_STORAGE_SUMMARY_TOTAL,
I18N_STORAGE_TITLE,
I18N_STORAGE_PRICE_PRE_UNIT,
I18N_STORAGE_TOOLTIP_NOTE,
planTags,
STORAGE_PER_PACK,
} from 'ee/subscriptions/buy_addons_shared/constants';
describe('Buy Storage App', () => {
let wrapper;
function createComponent(apolloProvider) {
wrapper = shallowMountExtended(App, {
localVue,
apolloProvider,
stubs: {
Checkout,
AddonPurchaseDetails,
OrderSummary,
SummaryDetails,
},
});
return waitForPromises();
}
const createComponent = () => {
wrapper = shallowMountExtended(App);
};
const getStoragePlan = () => pick(mockStoragePlans[0], ['id', 'code', 'pricePerYear', 'name']);
const findCheckout = () => wrapper.findComponent(Checkout);
const findOrderSummary = () => wrapper.findComponent(OrderSummary);
const findPriceLabel = () => wrapper.findByTestId('price-per-unit');
const findQuantityText = () => wrapper.findByTestId('addon-quantity-text');
const findSummaryLabel = () => wrapper.findByTestId('summary-label');
const findSummaryTotal = () => wrapper.findByTestId('summary-total');
const findBuyAddonsApp = () => wrapper.findComponent(BuyAddonsApp);
afterEach(() => {
wrapper.destroy();
beforeEach(() => {
createComponent();
});
describe('when data is received', () => {
beforeEach(() => {
const mockApollo = createMockApolloProvider();
return createComponent(mockApollo);
});
it('should display the StepOrderApp', () => {
expect(wrapper.findComponent(StepOrderApp).exists()).toBe(true);
expect(wrapper.findComponent(GlEmptyState).exists()).toBe(false);
});
it('provides the correct props to checkout', () => {
expect(findCheckout().props()).toMatchObject({
plan: { ...getStoragePlan, isAddon: true },
});
});
it('provides the correct props to order summary', () => {
expect(findOrderSummary().props()).toMatchObject({
plan: { ...getStoragePlan, isAddon: true },
title: I18N_STORAGE_TITLE,
});
});
});
describe('when data is not received', () => {
it('should display the GlEmptyState for empty data', async () => {
const mockApollo = createMockApolloProvider({
plansQueryMock: jest.fn().mockResolvedValue({ data: null }),
});
await createComponent(mockApollo);
expect(wrapper.findComponent(StepOrderApp).exists()).toBe(false);
expect(wrapper.findComponent(GlEmptyState).exists()).toBe(true);
});
it('should display the GlEmptyState for empty plans', async () => {
const mockApollo = createMockApolloProvider({
plansQueryMock: jest.fn().mockResolvedValue({ data: { plans: null } }),
});
await createComponent(mockApollo);
expect(wrapper.findComponent(StepOrderApp).exists()).toBe(false);
expect(wrapper.findComponent(GlEmptyState).exists()).toBe(true);
});
it('should display the GlEmptyState for plans data of wrong type', async () => {
const mockApollo = createMockApolloProvider({
plansQueryMock: jest.fn().mockResolvedValue({ data: { plans: {} } }),
});
await createComponent(mockApollo);
expect(wrapper.findComponent(StepOrderApp).exists()).toBe(false);
expect(wrapper.findComponent(GlEmptyState).exists()).toBe(true);
});
afterEach(() => {
wrapper.destroy();
});
describe('when an error is received', () => {
it('should display the GlEmptyState', async () => {
const mockApollo = createMockApolloProvider({
plansQueryMock: jest.fn().mockRejectedValue(new Error('An error happened!')),
});
await createComponent(mockApollo);
expect(wrapper.findComponent(StepOrderApp).exists()).toBe(false);
expect(wrapper.findComponent(GlEmptyState).exists()).toBe(true);
});
it('passes the correct tags', () => {
expect(findBuyAddonsApp().props('tags')).toEqual([planTags.STORAGE_PLAN]);
});
describe('labels', () => {
it('shows labels correctly for 1 pack', async () => {
const mockApollo = createMockApolloProvider();
await createComponent(mockApollo);
expect(findQuantityText().text()).toMatchInterpolatedText(
'x 10 GB per pack = 10 GB of storage',
);
expect(findSummaryLabel().text()).toBe('1 storage pack');
expect(findSummaryTotal().text()).toBe('Total storage: 10 GB');
expect(findPriceLabel().text()).toBe('$10 per 10 GB storage per pack');
});
it('shows labels correctly for 2 packs', async () => {
const mockApollo = createMockApolloProvider({}, { quantity: 2 });
await createComponent(mockApollo);
expect(findQuantityText().text()).toMatchInterpolatedText(
'x 10 GB per pack = 20 GB of storage',
);
expect(findSummaryLabel().text()).toBe('2 storage packs');
expect(findSummaryTotal().text()).toBe('Total storage: 20 GB');
});
it('does not show labels if input is invalid', async () => {
const mockApollo = createMockApolloProvider({}, { quantity: -1 });
await createComponent(mockApollo);
expect(findQuantityText().text()).toMatchInterpolatedText('x 10 GB per pack');
it('passes the correct config', () => {
expect(findBuyAddonsApp().props('config')).toMatchObject({
alertText: '',
formula: I18N_DETAILS_FORMULA,
formulaWithAlert: I18N_DETAILS_FORMULA_WITH_ALERT,
formulaTotal: I18N_STORAGE_FORMULA_TOTAL,
hasExpiration: true,
pricePerUnit: I18N_STORAGE_PRICE_PRE_UNIT,
productLabel: I18N_STORAGE_PRODUCT_LABEL,
productUnit: I18N_STORAGE_PRODUCT_UNIT,
quantityPerPack: STORAGE_PER_PACK,
summaryTitle: i18nStorageSummaryTitle,
summaryTotal: I18N_STORAGE_SUMMARY_TOTAL,
title: I18N_STORAGE_TITLE,
tooltipNote: I18N_STORAGE_TOOLTIP_NOTE,
});
});
});
......@@ -6742,6 +6742,9 @@ msgstr ""
msgid "Checkout|%{name}'s storage subscription"
msgstr ""
msgid "Checkout|%{quantity} CI minutes"
msgstr ""
msgid "Checkout|%{quantity} GB of storage"
msgstr ""
......@@ -6756,9 +6759,6 @@ msgstr ""
msgid "Checkout|%{startDate} - %{endDate}"
msgstr ""
msgid "Checkout|%{totalCiMinutes} CI minutes"
msgstr ""
msgid "Checkout|(may be %{linkStart}charged upon purchase%{linkEnd})"
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