Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Support
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
G
gitlab-ce
Project overview
Project overview
Details
Activity
Releases
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Issues
0
Issues
0
List
Boards
Labels
Milestones
Merge Requests
1
Merge Requests
1
Analytics
Analytics
Repository
Value Stream
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Create a new issue
Commits
Issue Boards
Open sidebar
nexedi
gitlab-ce
Commits
02321dcf
Commit
02321dcf
authored
Nov 30, 2021
by
Angelo Gulina
Committed by
Vitaly Slobodin
Nov 30, 2021
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Re-use common logic for addons purchase
parent
831b8720
Changes
10
Hide whitespace changes
Inline
Side-by-side
Showing
10 changed files
with
507 additions
and
578 deletions
+507
-578
ee/app/assets/javascripts/subscriptions/buy_addons_shared/components/app.vue
...cripts/subscriptions/buy_addons_shared/components/app.vue
+184
-0
ee/app/assets/javascripts/subscriptions/buy_addons_shared/components/checkout/addon_purchase_details.vue
...ons_shared/components/checkout/addon_purchase_details.vue
+3
-15
ee/app/assets/javascripts/subscriptions/buy_addons_shared/constants.js
.../javascripts/subscriptions/buy_addons_shared/constants.js
+1
-1
ee/app/assets/javascripts/subscriptions/buy_minutes/components/app.vue
.../javascripts/subscriptions/buy_minutes/components/app.vue
+25
-138
ee/app/assets/javascripts/subscriptions/buy_storage/components/app.vue
.../javascripts/subscriptions/buy_storage/components/app.vue
+22
-144
ee/spec/frontend/subscriptions/buy_addons_shared/app_spec.js
ee/spec/frontend/subscriptions/buy_addons_shared/app_spec.js
+180
-0
ee/spec/frontend/subscriptions/buy_addons_shared/components/checkout/addon_purchase_details_spec.js
...shared/components/checkout/addon_purchase_details_spec.js
+7
-4
ee/spec/frontend/subscriptions/buy_minutes/components/app_spec.js
...frontend/subscriptions/buy_minutes/components/app_spec.js
+41
-140
ee/spec/frontend/subscriptions/buy_storage/components/app_spec.js
...frontend/subscriptions/buy_storage/components/app_spec.js
+41
-133
locale/gitlab.pot
locale/gitlab.pot
+3
-3
No files found.
ee/app/assets/javascripts/subscriptions/buy_addons_shared/components/app.vue
0 → 100644
View file @
02321dcf
<
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>
ee/app/assets/javascripts/subscriptions/buy_addons_shared/components/checkout/addon_purchase_details.vue
View file @
02321dcf
...
@@ -2,7 +2,6 @@
...
@@ -2,7 +2,6 @@
import
{
GlAlert
,
GlFormInput
}
from
'
@gitlab/ui
'
;
import
{
GlAlert
,
GlFormInput
}
from
'
@gitlab/ui
'
;
import
{
STEPS
}
from
'
ee/subscriptions/constants
'
;
import
{
STEPS
}
from
'
ee/subscriptions/constants
'
;
import
updateState
from
'
ee/subscriptions/graphql/mutations/update_state.mutation.graphql
'
;
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
Step
from
'
ee/vue_shared/purchase_flow/components/step.vue
'
;
import
{
GENERAL_ERROR_MESSAGE
}
from
'
ee/vue_shared/purchase_flow/constants
'
;
import
{
GENERAL_ERROR_MESSAGE
}
from
'
ee/vue_shared/purchase_flow/constants
'
;
import
createFlash
from
'
~/flash
'
;
import
createFlash
from
'
~/flash
'
;
...
@@ -28,7 +27,7 @@ export default {
...
@@ -28,7 +27,7 @@ export default {
type
:
String
,
type
:
String
,
required
:
true
,
required
:
true
,
},
},
quantity
PerPack
:
{
quantity
:
{
type
:
Number
,
type
:
Number
,
required
:
true
,
required
:
true
,
},
},
...
@@ -42,14 +41,6 @@ export default {
...
@@ -42,14 +41,6 @@ export default {
default
:
''
,
default
:
''
,
},
},
},
},
apollo
:
{
quantity
:
{
query
:
stateQuery
,
update
(
data
)
{
return
data
.
subscription
.
quantity
;
},
},
},
computed
:
{
computed
:
{
quantityModel
:
{
quantityModel
:
{
get
()
{
get
()
{
...
@@ -62,9 +53,6 @@ export default {
...
@@ -62,9 +53,6 @@ export default {
isValid
()
{
isValid
()
{
return
this
.
quantity
>
0
;
return
this
.
quantity
>
0
;
},
},
totalUnits
()
{
return
this
.
quantity
*
this
.
quantityPerPack
;
},
},
},
methods
:
{
methods
:
{
updateQuantity
(
quantity
=
0
)
{
updateQuantity
(
quantity
=
0
)
{
...
@@ -121,12 +109,12 @@ export default {
...
@@ -121,12 +109,12 @@ export default {
class=
"gl-w-15"
class=
"gl-w-15"
/>
/>
<div
class=
"gl-ml-3"
data-testid=
"addon-quantity-text"
>
<div
class=
"gl-ml-3"
data-testid=
"addon-quantity-text"
>
<slot
name=
"formula"
:quantity=
"totalUnits"
></slot>
<slot
name=
"formula"
></slot>
</div>
</div>
</div>
</div>
</
template
>
</
template
>
<
template
#summary
>
<
template
#summary
>
<slot
name=
"summary-label"
:quantity=
"quantity"
></slot>
<slot
name=
"summary-label"
></slot>
</
template
>
</
template
>
</step>
</step>
</template>
</template>
ee/app/assets/javascripts/subscriptions/buy_addons_shared/constants.js
View file @
02321dcf
...
@@ -22,7 +22,7 @@ export const STORAGE_PER_PACK = 10;
...
@@ -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_LABEL
=
s__
(
'
Checkout|CI minute pack
'
);
export
const
I18N_CI_MINUTES_PRODUCT_UNIT
=
s__
(
'
Checkout|minutes
'
);
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
)
=>
export
const
i18nCIMinutesSummaryTitle
=
(
quantity
)
=>
n__
(
'
Checkout|%d CI minute pack
'
,
'
Checkout|%d CI minute packs
'
,
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}
'
);
export
const
I18N_CI_MINUTES_SUMMARY_TOTAL
=
s__
(
'
Checkout|Total minutes: %{quantity}
'
);
...
...
ee/app/assets/javascripts/subscriptions/buy_minutes/components/app.vue
View file @
02321dcf
<
script
>
<
script
>
import
emptySvg
from
'
@gitlab/svgs/dist/illustrations/security-dashboard-empty-state.svg
'
;
import
BuyAddonsApp
from
'
ee/subscriptions/buy_addons_shared/components/app.vue
'
;
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
{
import
{
CI_MINUTES_PER_PACK
,
planTags
,
I18N_CI_MINUTES_PRICE_PRE_UNIT
,
I18N_CI_MINUTES_PRODUCT_LABEL
,
I18N_CI_MINUTES_PRODUCT_LABEL
,
I18N_CI_MINUTES_PRODUCT_UNIT
,
I18N_CI_MINUTES_PRODUCT_UNIT
,
I18N_DETAILS_FORMULA
,
I18N_DETAILS_FORMULA
,
...
@@ -17,146 +12,38 @@ import {
...
@@ -17,146 +12,38 @@ import {
i18nCIMinutesSummaryTitle
,
i18nCIMinutesSummaryTitle
,
I18N_CI_MINUTES_SUMMARY_TOTAL
,
I18N_CI_MINUTES_SUMMARY_TOTAL
,
I18N_CI_MINUTES_ALERT_TEXT
,
I18N_CI_MINUTES_ALERT_TEXT
,
I18N_CI_MINUTES_PRICE_PRE_UNIT
,
I18N_CI_MINUTES_TITLE
,
I18N_CI_MINUTES_TITLE
,
planTags
,
}
from
'
ee/subscriptions/buy_addons_shared/constants
'
;
CUSTOMERSDOT_CLIENT
,
CI_MINUTES_PER_PACK
,
}
from
'
../../buy_addons_shared/constants
'
;
import
plansQuery
from
'
../../graphql/queries/plans.customer.query.graphql
'
;
export
default
{
export
default
{
components
:
{
components
:
{
Checkout
,
BuyAddonsApp
,
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
,
};
},
},
computed
:
{
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
{
return
{
...
this
.
plans
[
0
],
alertText
:
I18N_CI_MINUTES_ALERT_TEXT
,
isAddon
:
true
,
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
>
</
script
>
<
template
>
<
template
>
<gl-empty-state
<buy-addons-app
:config=
"config"
:tags=
"tags"
/>
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>
</
template
>
</
template
>
ee/app/assets/javascripts/subscriptions/buy_storage/components/app.vue
View file @
02321dcf
<
script
>
<
script
>
import
emptySvg
from
'
@gitlab/svgs/dist/illustrations/security-dashboard-empty-state.svg
'
;
import
BuyAddonsApp
from
'
ee/subscriptions/buy_addons_shared/components/app.vue
'
;
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
{
import
{
I18N_STORAGE_PRODUCT_LABEL
,
I18N_STORAGE_PRODUCT_LABEL
,
I18N_STORAGE_PRODUCT_UNIT
,
I18N_STORAGE_PRODUCT_UNIT
,
...
@@ -20,153 +12,39 @@ import {
...
@@ -20,153 +12,39 @@ import {
I18N_STORAGE_PRICE_PRE_UNIT
,
I18N_STORAGE_PRICE_PRE_UNIT
,
I18N_STORAGE_TOOLTIP_NOTE
,
I18N_STORAGE_TOOLTIP_NOTE
,
planTags
,
planTags
,
CUSTOMERSDOT_CLIENT
,
STORAGE_PER_PACK
,
STORAGE_PER_PACK
,
}
from
'
../../buy_addons_shared/constants
'
;
}
from
'
ee/subscriptions/buy_addons_shared/constants
'
;
import
plansQuery
from
'
../../graphql/queries/plans.customer.query.graphql
'
;
export
default
{
export
default
{
name
:
'
BuyStorageApp
'
,
name
:
'
BuyStorageApp
'
,
components
:
{
components
:
{
Checkout
,
BuyAddonsApp
,
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
,
};
},
},
computed
:
{
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
{
return
{
...
this
.
plans
[
0
],
alertText
:
''
,
isAddon
:
true
,
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
>
</
script
>
<
template
>
<
template
>
<gl-empty-state
<buy-addons-app
:config=
"config"
:tags=
"tags"
/>
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>
</
template
>
</
template
>
ee/spec/frontend/subscriptions/buy_addons_shared/app_spec.js
0 → 100644
View file @
02321dcf
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
'
);
});
});
});
ee/spec/frontend/subscriptions/buy_addons_shared/components/checkout/addon_purchase_details_spec.js
View file @
02321dcf
...
@@ -38,7 +38,7 @@ describe('AddonPurchaseDetails', () => {
...
@@ -38,7 +38,7 @@ describe('AddonPurchaseDetails', () => {
},
},
propsData
:
{
propsData
:
{
productLabel
:
'
CI minute pack
'
,
productLabel
:
'
CI minute pack
'
,
quantity
PerPack
:
100
0
,
quantity
:
1
0
,
packsFormula
:
'
x %{packQuantity} minutes per pack = %{strong}
'
,
packsFormula
:
'
x %{packQuantity} minutes per pack = %{strong}
'
,
quantityText
:
'
%{quantity} CI minutes
'
,
quantityText
:
'
%{quantity} CI minutes
'
,
totalPurchase
:
'
Total minutes: %{quantity}
'
,
totalPurchase
:
'
Total minutes: %{quantity}
'
,
...
@@ -73,9 +73,12 @@ describe('AddonPurchaseDetails', () => {
...
@@ -73,9 +73,12 @@ describe('AddonPurchaseDetails', () => {
});
});
it
(
'
is invalid when quantity is less than 1
'
,
async
()
=>
{
it
(
'
is invalid when quantity is less than 1
'
,
async
()
=>
{
createComponent
({
createComponent
(
subscription
:
{
namespaceId
:
483
,
quantity
:
0
},
{
});
subscription
:
{
namespaceId
:
483
},
},
{
quantity
:
0
},
);
expect
(
isStepValid
()).
toBe
(
false
);
expect
(
isStepValid
()).
toBe
(
false
);
});
});
...
...
ee/spec/frontend/subscriptions/buy_minutes/components/app_spec.js
View file @
02321dcf
import
{
GlEmptyState
}
from
'
@gitlab/ui
'
;
import
BuyAddonsApp
from
'
ee/subscriptions/buy_addons_shared/components/app.vue
'
;
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
{
shallowMountExtended
}
from
'
helpers/vue_test_utils_helper
'
;
import
{
shallowMountExtended
}
from
'
helpers/vue_test_utils_helper
'
;
import
waitForPromises
from
'
helpers/wait_for_promises
'
;
import
App
from
'
ee/subscriptions/buy_minutes/components/app.vue
'
;
import
{
createMockApolloProvider
}
from
'
ee_jest/subscriptions/spec_helper
'
;
import
{
import
{
mockCiMinutesPlans
}
from
'
ee_jest/subscriptions/mock_data
'
;
CI_MINUTES_PER_PACK
,
planTags
,
const
localVue
=
createLocalVue
();
I18N_CI_MINUTES_PRICE_PRE_UNIT
,
localVue
.
use
(
VueApollo
);
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
'
,
()
=>
{
describe
(
'
Buy Minutes App
'
,
()
=>
{
let
wrapper
;
let
wrapper
;
function
createComponent
(
apolloProvider
)
{
const
createComponent
=
()
=>
{
return
shallowMountExtended
(
App
,
{
wrapper
=
shallowMountExtended
(
App
);
localVue
,
};
apolloProvider
,
stubs
:
{
Checkout
,
AddonPurchaseDetails
,
OrderSummary
,
SummaryDetails
,
},
});
}
const
getCiMinutePlan
=
()
=>
pick
(
mockCiMinutesPlans
[
0
],
[
'
id
'
,
'
code
'
,
'
pricePerYear
'
,
'
name
'
]);
const
findBuyAddonsApp
=
()
=>
wrapper
.
findComponent
(
BuyAddonsApp
);
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
'
);
after
Each
(()
=>
{
before
Each
(()
=>
{
wrapper
.
destroy
();
createComponent
();
});
});
describe
(
'
when data is received
'
,
()
=>
{
afterEach
(()
=>
{
beforeEach
(()
=>
{
wrapper
.
destroy
();
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
);
});
});
});
describe
(
'
when an error is received
'
,
()
=>
{
it
(
'
passes the correct tags
'
,
()
=>
{
it
(
'
should display the GlEmptyState
'
,
async
()
=>
{
expect
(
findBuyAddonsApp
().
props
(
'
tags
'
)).
toEqual
([
planTags
.
CI_1000_MINUTES_PLAN
]);
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
);
});
});
});
describe
(
'
labels
'
,
()
=>
{
it
(
'
passes the correct config
'
,
()
=>
{
it
(
'
shows labels correctly for 1 pack
'
,
async
()
=>
{
expect
(
findBuyAddonsApp
().
props
(
'
config
'
)).
toMatchObject
({
const
mockApollo
=
createMockApolloProvider
();
alertText
:
I18N_CI_MINUTES_ALERT_TEXT
,
wrapper
=
createComponent
(
mockApollo
);
formula
:
I18N_DETAILS_FORMULA
,
await
waitForPromises
();
formulaWithAlert
:
I18N_DETAILS_FORMULA_WITH_ALERT
,
formulaTotal
:
I18N_CI_MINUTES_FORMULA_TOTAL
,
expect
(
findQuantityText
().
text
()).
toMatchInterpolatedText
(
hasExpiration
:
false
,
'
x 1,000 minutes per pack = 1,000 CI minutes
'
,
pricePerUnit
:
I18N_CI_MINUTES_PRICE_PRE_UNIT
,
);
productLabel
:
I18N_CI_MINUTES_PRODUCT_LABEL
,
expect
(
findSummaryLabel
().
text
()).
toBe
(
'
1 CI minute pack
'
);
productUnit
:
I18N_CI_MINUTES_PRODUCT_UNIT
,
expect
(
findSummaryTotal
().
text
()).
toBe
(
'
Total minutes: 1,000
'
);
quantityPerPack
:
CI_MINUTES_PER_PACK
,
expect
(
findPriceLabel
().
text
()).
toBe
(
'
$10 per pack of 1,000 minutes
'
);
summaryTitle
:
i18nCIMinutesSummaryTitle
,
});
summaryTotal
:
I18N_CI_MINUTES_SUMMARY_TOTAL
,
title
:
I18N_CI_MINUTES_TITLE
,
it
(
'
shows labels correctly for 2 packs
'
,
async
()
=>
{
tooltipNote
:
''
,
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
'
);
});
});
});
});
});
});
ee/spec/frontend/subscriptions/buy_storage/components/app_spec.js
View file @
02321dcf
import
{
GlEmptyState
}
from
'
@gitlab/ui
'
;
import
BuyAddonsApp
from
'
ee/subscriptions/buy_addons_shared/components/app.vue
'
;
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
{
shallowMountExtended
}
from
'
helpers/vue_test_utils_helper
'
;
import
{
shallowMountExtended
}
from
'
helpers/vue_test_utils_helper
'
;
import
waitForPromises
from
'
helpers/wait_for_promises
'
;
import
App
from
'
ee/subscriptions/buy_storage/components/app.vue
'
;
import
{
createMockApolloProvider
}
from
'
ee_jest/subscriptions/spec_helper
'
;
import
{
import
{
mockStoragePlans
}
from
'
ee_jest/subscriptions/mock_data
'
;
I18N_STORAGE_PRODUCT_LABEL
,
I18N_STORAGE_PRODUCT_UNIT
,
const
localVue
=
createLocalVue
();
I18N_DETAILS_FORMULA
,
localVue
.
use
(
VueApollo
);
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
'
,
()
=>
{
describe
(
'
Buy Storage App
'
,
()
=>
{
let
wrapper
;
let
wrapper
;
function
createComponent
(
apolloProvider
)
{
const
createComponent
=
()
=>
{
wrapper
=
shallowMountExtended
(
App
,
{
wrapper
=
shallowMountExtended
(
App
);
localVue
,
};
apolloProvider
,
stubs
:
{
Checkout
,
AddonPurchaseDetails
,
OrderSummary
,
SummaryDetails
,
},
});
return
waitForPromises
();
}
const
getStoragePlan
=
()
=>
pick
(
mockStoragePlans
[
0
],
[
'
id
'
,
'
code
'
,
'
pricePerYear
'
,
'
name
'
]);
const
findBuyAddonsApp
=
()
=>
wrapper
.
findComponent
(
BuyAddonsApp
);
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
'
);
after
Each
(()
=>
{
before
Each
(()
=>
{
wrapper
.
destroy
();
createComponent
();
});
});
describe
(
'
when data is received
'
,
()
=>
{
afterEach
(()
=>
{
beforeEach
(()
=>
{
wrapper
.
destroy
();
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
);
});
});
});
describe
(
'
when an error is received
'
,
()
=>
{
it
(
'
passes the correct tags
'
,
()
=>
{
it
(
'
should display the GlEmptyState
'
,
async
()
=>
{
expect
(
findBuyAddonsApp
().
props
(
'
tags
'
)).
toEqual
([
planTags
.
STORAGE_PLAN
]);
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
);
});
});
});
describe
(
'
labels
'
,
()
=>
{
it
(
'
passes the correct config
'
,
()
=>
{
it
(
'
shows labels correctly for 1 pack
'
,
async
()
=>
{
expect
(
findBuyAddonsApp
().
props
(
'
config
'
)).
toMatchObject
({
const
mockApollo
=
createMockApolloProvider
();
alertText
:
''
,
await
createComponent
(
mockApollo
);
formula
:
I18N_DETAILS_FORMULA
,
formulaWithAlert
:
I18N_DETAILS_FORMULA_WITH_ALERT
,
expect
(
findQuantityText
().
text
()).
toMatchInterpolatedText
(
formulaTotal
:
I18N_STORAGE_FORMULA_TOTAL
,
'
x 10 GB per pack = 10 GB of storage
'
,
hasExpiration
:
true
,
);
pricePerUnit
:
I18N_STORAGE_PRICE_PRE_UNIT
,
expect
(
findSummaryLabel
().
text
()).
toBe
(
'
1 storage pack
'
);
productLabel
:
I18N_STORAGE_PRODUCT_LABEL
,
expect
(
findSummaryTotal
().
text
()).
toBe
(
'
Total storage: 10 GB
'
);
productUnit
:
I18N_STORAGE_PRODUCT_UNIT
,
expect
(
findPriceLabel
().
text
()).
toBe
(
'
$10 per 10 GB storage per pack
'
);
quantityPerPack
:
STORAGE_PER_PACK
,
});
summaryTitle
:
i18nStorageSummaryTitle
,
summaryTotal
:
I18N_STORAGE_SUMMARY_TOTAL
,
it
(
'
shows labels correctly for 2 packs
'
,
async
()
=>
{
title
:
I18N_STORAGE_TITLE
,
const
mockApollo
=
createMockApolloProvider
({},
{
quantity
:
2
});
tooltipNote
:
I18N_STORAGE_TOOLTIP_NOTE
,
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
'
);
});
});
});
});
});
});
locale/gitlab.pot
View file @
02321dcf
...
@@ -6742,6 +6742,9 @@ msgstr ""
...
@@ -6742,6 +6742,9 @@ msgstr ""
msgid "Checkout|%{name}'s storage subscription"
msgid "Checkout|%{name}'s storage subscription"
msgstr ""
msgstr ""
msgid "Checkout|%{quantity} CI minutes"
msgstr ""
msgid "Checkout|%{quantity} GB of storage"
msgid "Checkout|%{quantity} GB of storage"
msgstr ""
msgstr ""
...
@@ -6756,9 +6759,6 @@ msgstr ""
...
@@ -6756,9 +6759,6 @@ msgstr ""
msgid "Checkout|%{startDate} - %{endDate}"
msgid "Checkout|%{startDate} - %{endDate}"
msgstr ""
msgstr ""
msgid "Checkout|%{totalCiMinutes} CI minutes"
msgstr ""
msgid "Checkout|(may be %{linkStart}charged upon purchase%{linkEnd})"
msgid "Checkout|(may be %{linkStart}charged upon purchase%{linkEnd})"
msgstr ""
msgstr ""
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment