mirror of
https://github.com/overleaf/overleaf.git
synced 2025-04-20 06:13:40 +00:00
Merge pull request #11872 from overleaf/ii-payment-page-tests
[web] Payment page tests GitOrigin-RevId: 0ab9a75c13f1833cbdf7aa71ffe3ab66174ca773
This commit is contained in:
parent
612728d300
commit
076bc9b39c
13 changed files with 1073 additions and 11 deletions
|
@ -18,6 +18,7 @@ import SubmitButton from './submit-button'
|
|||
import ThreeDSecure from './three-d-secure'
|
||||
import getMeta from '../../../../../utils/meta'
|
||||
import { postJSON } from '../../../../../infrastructure/fetch-json'
|
||||
import { assign } from '../../../../../shared/components/location'
|
||||
import * as eventTracking from '../../../../../infrastructure/event-tracking'
|
||||
import classnames from 'classnames'
|
||||
import {
|
||||
|
@ -153,7 +154,7 @@ function CheckoutPanel() {
|
|||
'subscription-submission-success',
|
||||
planCode
|
||||
)
|
||||
window.location.assign('/user/subscription/thank-you')
|
||||
assign('/user/subscription/thank-you')
|
||||
} catch (error) {
|
||||
setIsProcessing(false)
|
||||
|
||||
|
@ -184,6 +185,8 @@ function CheckoutPanel() {
|
|||
const payPal = useRef<PayPalInstance>()
|
||||
|
||||
useEffect(() => {
|
||||
if (!recurly) return
|
||||
|
||||
payPal.current = recurly.PayPal({
|
||||
display: { displayName: planName },
|
||||
})
|
||||
|
@ -304,6 +307,7 @@ function CheckoutPanel() {
|
|||
onSubmit={handleSubmit}
|
||||
onChange={handleFormValidation}
|
||||
ref={formRef}
|
||||
data-testid="checkout-form"
|
||||
>
|
||||
{genericError && (
|
||||
<Alert bsStyle="warning" className="small">
|
||||
|
|
|
@ -11,7 +11,10 @@ function PaymentMethodToggle(props: PaymentMethodToggleProps) {
|
|||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<FormGroup className="payment-method-toggle">
|
||||
<FormGroup
|
||||
className="payment-method-toggle"
|
||||
data-testid="payment-method-toggle"
|
||||
>
|
||||
<hr className="thin" />
|
||||
<div className="radio">
|
||||
<Col xs={8}>
|
||||
|
|
|
@ -26,7 +26,7 @@ function CurrencyDropdown(props: DropdownProps) {
|
|||
>
|
||||
{currency === currencyCode && (
|
||||
<span className="change-currency-dropdown-selected-icon">
|
||||
<Icon type="check" />
|
||||
<Icon type="check" accessibilityLabel={t('selected')} />
|
||||
</span>
|
||||
)}
|
||||
{currency} ({symbol})
|
||||
|
|
|
@ -11,7 +11,7 @@ function FeaturesList({ features }: FeaturesListProps) {
|
|||
return (
|
||||
<>
|
||||
<div className="text-small">{t('all_premium_features_including')}</div>
|
||||
<ul className="small">
|
||||
<ul className="small" data-testid="features-list">
|
||||
{features.compileTimeout > 1 && (
|
||||
<li>{t('increased_compile_timeout')}</li>
|
||||
)}
|
||||
|
|
|
@ -24,10 +24,10 @@ function PriceSummary() {
|
|||
return (
|
||||
<>
|
||||
<hr />
|
||||
<div className="price-summary">
|
||||
<div className="price-summary" data-testid="price-summary">
|
||||
<h4>{t('payment_summary')}</h4>
|
||||
<div className="small">
|
||||
<div className="price-summary-line">
|
||||
<div className="price-summary-line" data-testid="price-summary-plan">
|
||||
<span>{planName}</span>
|
||||
<span>
|
||||
{currencySymbol}
|
||||
|
@ -35,7 +35,10 @@ function PriceSummary() {
|
|||
</span>
|
||||
</div>
|
||||
{coupon && (
|
||||
<div className="price-summary-line">
|
||||
<div
|
||||
className="price-summary-line"
|
||||
data-testid="price-summary-coupon"
|
||||
>
|
||||
<span>{coupon.name}</span>
|
||||
<span aria-hidden>
|
||||
–{currencySymbol}
|
||||
|
@ -49,7 +52,7 @@ function PriceSummary() {
|
|||
</div>
|
||||
)}
|
||||
{rate > 0 && (
|
||||
<div className="price-summary-line">
|
||||
<div className="price-summary-line" data-testid="price-summary-vat">
|
||||
<span>
|
||||
{t('vat')} {rate * 100}%
|
||||
</span>
|
||||
|
@ -59,7 +62,10 @@ function PriceSummary() {
|
|||
</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="price-summary-line price-summary-total-line">
|
||||
<div
|
||||
className="price-summary-line price-summary-total-line"
|
||||
data-testid="price-summary-total"
|
||||
>
|
||||
<b>{monthlyBilling ? t('total_per_month') : t('total_per_year')}</b>
|
||||
<b>
|
||||
{currencySymbol}
|
||||
|
|
|
@ -14,7 +14,9 @@ function TrialCouponSummary() {
|
|||
return (
|
||||
<>
|
||||
<hr className="thin" />
|
||||
<div className="trial-coupon-summary">{children}</div>
|
||||
<div className="trial-coupon-summary" data-testid="trial-coupon-summary">
|
||||
{children}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -339,7 +339,7 @@ export const PaymentContext = createContext<PaymentContextValue | undefined>(
|
|||
|
||||
type PaymentProviderProps = {
|
||||
publicKey: string
|
||||
children: React.ReactNode
|
||||
children?: React.ReactNode
|
||||
}
|
||||
|
||||
export function PaymentProvider({ publicKey, ...props }: PaymentProviderProps) {
|
||||
|
|
|
@ -0,0 +1,465 @@
|
|||
import CheckoutPanel from '../../../../../../frontend/js/features/subscription/components/new/checkout/checkout-panel'
|
||||
import { PaymentProvider } from '../../../../../../frontend/js/features/subscription/context/payment-context'
|
||||
import { plans } from '../../fixtures/plans'
|
||||
import {
|
||||
createFakeRecurly,
|
||||
defaultSubscription,
|
||||
ElementsBase,
|
||||
} from '../../fixtures/recurly-mock'
|
||||
import { cloneDeep } from 'lodash'
|
||||
import { TokenHandler, RecurlyError } from 'recurly__recurly-js'
|
||||
|
||||
function CheckoutPanelWithPaymentProvider() {
|
||||
return (
|
||||
<PaymentProvider publicKey="0000">
|
||||
<CheckoutPanel />
|
||||
</PaymentProvider>
|
||||
)
|
||||
}
|
||||
|
||||
function fillForm() {
|
||||
cy.findByTestId('test-card-element').within(() => {
|
||||
cy.get('input').each(el => {
|
||||
cy.wrap(el).type('123', { delay: 0 })
|
||||
})
|
||||
})
|
||||
cy.findByLabelText(/first name/i).type('123', { delay: 0 })
|
||||
cy.findByLabelText(/last name/i).type('123', { delay: 0 })
|
||||
cy.findByLabelText('Address').type('123', { delay: 0 })
|
||||
cy.findByLabelText(/postal code/i).type('123', { delay: 0 })
|
||||
cy.findByLabelText(/country/i).select('Bulgaria')
|
||||
}
|
||||
|
||||
describe('checkout panel', function () {
|
||||
const itmCampaign = 'fake_itm_campaign'
|
||||
const itmContent = 'fake_itm_content'
|
||||
const itmReferrer = 'fake_itm_referrer'
|
||||
|
||||
beforeEach(function () {
|
||||
const plan = plans.find(({ planCode }) => planCode === 'student-annual')
|
||||
|
||||
if (!plan) {
|
||||
throw new Error('No plan was found while running the test!')
|
||||
}
|
||||
|
||||
cy.window().then(win => {
|
||||
win.metaAttributesCache = new Map()
|
||||
win.metaAttributesCache.set('ol-countryCode', '')
|
||||
win.metaAttributesCache.set('ol-recurlyApiKey', '0000')
|
||||
win.metaAttributesCache.set('ol-recommendedCurrency', 'USD')
|
||||
win.metaAttributesCache.set('ol-plan', plan)
|
||||
win.metaAttributesCache.set('ol-planCode', plan.planCode)
|
||||
win.metaAttributesCache.set('ol-showCouponField', true)
|
||||
win.metaAttributesCache.set('ol-itm_campaign', itmCampaign)
|
||||
win.metaAttributesCache.set('ol-itm_content', itmContent)
|
||||
win.metaAttributesCache.set('ol-itm_referrer', itmReferrer)
|
||||
|
||||
cy.wrap(plan).as('plan')
|
||||
|
||||
// init default recurly
|
||||
win.recurly = createFakeRecurly(defaultSubscription)
|
||||
|
||||
cy.interceptEvents()
|
||||
})
|
||||
|
||||
cy.mount(<CheckoutPanelWithPaymentProvider />)
|
||||
cy.findByTestId('checkout-form').as('form')
|
||||
})
|
||||
|
||||
it('renders heading', function () {
|
||||
cy.contains(/select a payment method/i)
|
||||
})
|
||||
|
||||
it('renders student disclaimer', function () {
|
||||
cy.contains(
|
||||
'The educational discount applies to all students at secondary and postsecondary institutions ' +
|
||||
'(schools and universities). We may contact you to confirm that you’re eligible for the discount.'
|
||||
)
|
||||
})
|
||||
|
||||
it('renders payment method toggle', function () {
|
||||
cy.findByTestId('payment-method-toggle').within(() => {
|
||||
cy.findByLabelText(/card payment/i)
|
||||
cy.findByLabelText(/paypal/i)
|
||||
})
|
||||
})
|
||||
|
||||
it('renders address first line input', function () {
|
||||
cy.get('@form').within(() => {
|
||||
cy.findByLabelText('Address')
|
||||
cy.findByLabelText(/this address will be shown on the invoice/i)
|
||||
})
|
||||
})
|
||||
|
||||
it('renders address second line input', function () {
|
||||
cy.get('@form').within(() => {
|
||||
cy.findByLabelText(/address second line/i).should('not.exist')
|
||||
cy.findByRole('button', { name: /add another address line/i }).click()
|
||||
cy.findByLabelText(/address second line/i)
|
||||
})
|
||||
})
|
||||
|
||||
it('renders postal code input', function () {
|
||||
cy.get('@form').within(() => {
|
||||
cy.findByLabelText(/postal code/i)
|
||||
})
|
||||
})
|
||||
|
||||
it('renders country dropdown', function () {
|
||||
cy.get('@form').within(() => {
|
||||
cy.findByLabelText(/country/i)
|
||||
})
|
||||
})
|
||||
|
||||
it('renders company details', function () {
|
||||
cy.get('@form').within(() => {
|
||||
cy.findByLabelText(/add company details/i).as('checkbox')
|
||||
cy.get('@checkbox').should('not.be.checked')
|
||||
cy.findByLabelText(/company name/i).should('not.exist')
|
||||
cy.findByLabelText(/vat number/i).should('not.exist')
|
||||
cy.get('@checkbox').click()
|
||||
cy.findByLabelText(/company name/i)
|
||||
cy.findByLabelText(/vat number/i)
|
||||
})
|
||||
})
|
||||
|
||||
it('renders coupon field', function () {
|
||||
cy.get('@form').within(() => {
|
||||
cy.findByLabelText(/coupon code/i)
|
||||
})
|
||||
})
|
||||
|
||||
it('renders tos agreement notice', function () {
|
||||
cy.contains(/by subscribing, you agree to our terms of service/i)
|
||||
})
|
||||
|
||||
it('renders recurly error', function () {
|
||||
cy.window().then(win => {
|
||||
win.recurly = undefined!
|
||||
})
|
||||
cy.mount(<CheckoutPanelWithPaymentProvider />)
|
||||
cy.contains(
|
||||
/sorry, there was an error talking to our payment provider. Please try again in a few moments/i
|
||||
)
|
||||
cy.contains(
|
||||
/if you are using any ad or script blocking extensions in your browser, you may need to temporarily disable them/i
|
||||
)
|
||||
})
|
||||
|
||||
it('calls recurly.token on submit', function () {
|
||||
cy.window().then(win => {
|
||||
cy.stub(win.recurly, 'token')
|
||||
})
|
||||
cy.mount(<CheckoutPanelWithPaymentProvider />)
|
||||
cy.get('@form').within(() => fillForm())
|
||||
cy.findByRole('button', { name: /upgrade now/i }).click()
|
||||
|
||||
cy.window().then(win => {
|
||||
expect(win.recurly.token).to.be.calledOnceWith(
|
||||
Cypress.sinon.match.instanceOf(ElementsBase),
|
||||
{
|
||||
first_name: '123',
|
||||
last_name: '123',
|
||||
postal_code: '123',
|
||||
address1: '123',
|
||||
address2: '',
|
||||
state: '',
|
||||
city: '',
|
||||
country: 'BG',
|
||||
coupon: '',
|
||||
},
|
||||
Cypress.sinon.match.func
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
it('renders generic error', function () {
|
||||
const errorMessage = 'generic error'
|
||||
cy.window().then(win => {
|
||||
win.recurly = createFakeRecurly(defaultSubscription, {
|
||||
token: (_1: unknown, _2: unknown, handler: TokenHandler) => {
|
||||
const err = new Error(errorMessage) as RecurlyError
|
||||
setTimeout(() => handler(err, { id: '1', type: 'abc' }), 100)
|
||||
},
|
||||
})
|
||||
})
|
||||
cy.mount(<CheckoutPanelWithPaymentProvider />)
|
||||
cy.get('@form').within(() => fillForm())
|
||||
|
||||
cy.findByRole('button', { name: /upgrade now/i }).as('button')
|
||||
cy.get('@button').click()
|
||||
cy.get('@button').within(() => {
|
||||
cy.contains(/processing/i)
|
||||
})
|
||||
cy.findByRole('alert').should('have.text', errorMessage)
|
||||
cy.get('@button').within(() => {
|
||||
cy.findByText(/processing/i).should('not.exist')
|
||||
})
|
||||
})
|
||||
|
||||
it('renders prefilled coupon input', function () {
|
||||
const couponCode = 'promo_code'
|
||||
cy.window().then(win => {
|
||||
win.metaAttributesCache.set('ol-couponCode', couponCode)
|
||||
})
|
||||
cy.mount(<CheckoutPanelWithPaymentProvider />)
|
||||
cy.findByLabelText(/coupon code/i).should('have.value', couponCode)
|
||||
})
|
||||
|
||||
it('calls coupon method when entering coupon code', function () {
|
||||
const couponCode = 'promo_code'
|
||||
cy.window().then(win => {
|
||||
const couponStub = cy.stub().as('coupon')
|
||||
couponStub.returnsThis()
|
||||
win.recurly = createFakeRecurly({
|
||||
...defaultSubscription,
|
||||
coupon: couponStub,
|
||||
})
|
||||
})
|
||||
cy.mount(<CheckoutPanelWithPaymentProvider />)
|
||||
cy.get('@coupon').should('be.calledOnce')
|
||||
cy.findByTestId('checkout-form').within(() => {
|
||||
cy.findByLabelText(/coupon code/i)
|
||||
.type(couponCode, { delay: 0 })
|
||||
.blur()
|
||||
})
|
||||
cy.get('@coupon').should('be.calledTwice').and('be.calledWith', couponCode)
|
||||
})
|
||||
|
||||
it('enters invalid coupon code', function () {
|
||||
cy.window().then(win => {
|
||||
const catchStub = cy.stub().as('catch')
|
||||
catchStub.onFirstCall().returnsThis()
|
||||
catchStub
|
||||
.onSecondCall()
|
||||
.callsFake(function (this: unknown, cb: (err: RecurlyError) => void) {
|
||||
const err = {
|
||||
name: 'api-error',
|
||||
code: 'not-found',
|
||||
} as RecurlyError
|
||||
|
||||
cb(err)
|
||||
return this
|
||||
})
|
||||
|
||||
win.recurly = createFakeRecurly({
|
||||
...defaultSubscription,
|
||||
catch: catchStub,
|
||||
})
|
||||
})
|
||||
cy.mount(<CheckoutPanelWithPaymentProvider />)
|
||||
cy.findByTestId('checkout-form').within(() => {
|
||||
cy.findByLabelText(/coupon code/i)
|
||||
.type('promo_code', { delay: 0 })
|
||||
.blur()
|
||||
})
|
||||
cy.findByRole('alert').within(() => {
|
||||
cy.contains(/coupon code is not valid for selected plan/i)
|
||||
})
|
||||
})
|
||||
|
||||
it('fails coupon verification', function () {
|
||||
cy.window().then(win => {
|
||||
const catchStub = cy.stub().as('catch')
|
||||
// call original method on change event
|
||||
catchStub.onFirstCall().returnsThis()
|
||||
catchStub
|
||||
.onSecondCall()
|
||||
.callsFake(function (this: unknown, cb: (err: RecurlyError) => void) {
|
||||
const err = {} as RecurlyError
|
||||
|
||||
try {
|
||||
cb(err)
|
||||
} catch (e) {}
|
||||
|
||||
return this
|
||||
})
|
||||
|
||||
win.recurly = createFakeRecurly({
|
||||
...defaultSubscription,
|
||||
catch: catchStub,
|
||||
})
|
||||
})
|
||||
cy.mount(<CheckoutPanelWithPaymentProvider />)
|
||||
cy.get('@catch').should('be.calledOnce')
|
||||
cy.findByTestId('checkout-form').within(() => {
|
||||
cy.findByLabelText(/coupon code/i)
|
||||
.type('promo_code', { delay: 0 })
|
||||
.blur()
|
||||
})
|
||||
cy.get('@catch').should('be.calledTwice')
|
||||
cy.findByRole('alert').within(() => {
|
||||
cy.contains(/an error occurred when verifying the coupon code/i)
|
||||
})
|
||||
})
|
||||
|
||||
/* The test is disabled due to https://github.com/overleaf/internal/issues/12004
|
||||
it.skip('creates a new subscription', function () {
|
||||
cy.stub(locationModule, 'assign').as('assign')
|
||||
cy.intercept('POST', 'user/subscription/create', {
|
||||
statusCode: 201,
|
||||
}).as('create')
|
||||
|
||||
cy.mount(<CheckoutPanelWithPaymentProvider />)
|
||||
cy.findByTestId('checkout-form').within(() => fillForm())
|
||||
cy.findByRole('button', { name: /upgrade now/i }).click()
|
||||
// verify itm params are also passed
|
||||
cy.get('@create')
|
||||
.its('request.body.subscriptionDetails')
|
||||
.should('contain', {
|
||||
ITMCampaign: itmCampaign,
|
||||
ITMContent: itmContent,
|
||||
ITMReferrer: itmReferrer,
|
||||
})
|
||||
cy.get('@assign')
|
||||
.should('be.calledOnce')
|
||||
.and('be.calledWith', '/user/subscription/thank-you')
|
||||
})
|
||||
*/
|
||||
|
||||
it('fails to create a new subscription', function () {
|
||||
cy.intercept('POST', 'user/subscription/create', {
|
||||
statusCode: 404,
|
||||
})
|
||||
|
||||
cy.mount(<CheckoutPanelWithPaymentProvider />)
|
||||
cy.findByTestId('checkout-form').within(() => fillForm())
|
||||
cy.findByRole('button', { name: /upgrade now/i }).click()
|
||||
cy.findByRole('alert').within(() => {
|
||||
cy.contains(/something went wrong processing the request/i)
|
||||
})
|
||||
})
|
||||
|
||||
describe('3D challenge', function () {
|
||||
it('shows three d secure challenge', function () {
|
||||
cy.intercept('POST', 'user/subscription/create', {
|
||||
statusCode: 404,
|
||||
body: {
|
||||
threeDSecureActionTokenId: '123',
|
||||
},
|
||||
})
|
||||
|
||||
cy.mount(<CheckoutPanelWithPaymentProvider />)
|
||||
cy.findByTestId('checkout-form').within(() => fillForm())
|
||||
cy.findByRole('button', { name: /upgrade now/i }).click()
|
||||
cy.findByRole('alert').within(() => {
|
||||
cy.contains(
|
||||
/your card must be authenticated with 3D Secure before continuing/i
|
||||
)
|
||||
})
|
||||
cy.contains('3D challenge content')
|
||||
})
|
||||
})
|
||||
|
||||
describe('card payments', function () {
|
||||
beforeEach(function () {
|
||||
cy.findByLabelText(/card payment/i).click()
|
||||
})
|
||||
|
||||
it('renders card element', function () {
|
||||
cy.get('@form').within(() => {
|
||||
cy.findByText(/card details/i, { selector: 'label' })
|
||||
cy.findByTestId('test-card-element')
|
||||
})
|
||||
})
|
||||
|
||||
it('verifies the card element does not disappear when switching between payment methods', function () {
|
||||
cy.get('@form').within(() => {
|
||||
cy.findByText(/card details/i, { selector: 'label' })
|
||||
cy.findByTestId('test-card-element')
|
||||
cy.findByLabelText(/paypal/i).click()
|
||||
cy.findByLabelText(/card payment/i).click()
|
||||
cy.findByText(/card details/i, { selector: 'label' })
|
||||
cy.findByTestId('test-card-element')
|
||||
})
|
||||
})
|
||||
|
||||
it('renders first name input', function () {
|
||||
cy.get('@form').within(() => {
|
||||
cy.findByLabelText(/first name/i)
|
||||
})
|
||||
})
|
||||
|
||||
it('renders last name input', function () {
|
||||
cy.get('@form').within(() => {
|
||||
cy.findByLabelText(/last name/i)
|
||||
})
|
||||
})
|
||||
|
||||
describe('submit button', function () {
|
||||
it('renders trial button', function () {
|
||||
cy.get('@form').within(() => {
|
||||
cy.findByRole('button', { name: /upgrade now, pay after \d+ days/i })
|
||||
})
|
||||
})
|
||||
|
||||
it('renders non-trial button', function () {
|
||||
cy.window().then(win => {
|
||||
const clone = cloneDeep(defaultSubscription)
|
||||
clone.items.plan!.trial = undefined
|
||||
win.recurly = createFakeRecurly(clone)
|
||||
})
|
||||
cy.mount(<CheckoutPanelWithPaymentProvider />)
|
||||
cy.findByTestId('checkout-form').within(() => {
|
||||
cy.findByRole('button', { name: 'Upgrade Now' })
|
||||
})
|
||||
})
|
||||
|
||||
it('handles the disabled state of submit button', function () {
|
||||
cy.get('@form').within(() => {
|
||||
cy.findByRole('button', { name: /upgrade now/i }).should(
|
||||
'be.disabled'
|
||||
)
|
||||
fillForm()
|
||||
cy.findByRole('button', { name: /upgrade now/i }).should(
|
||||
'not.be.disabled'
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('paypal payments', function () {
|
||||
beforeEach(function () {
|
||||
cy.findByLabelText(/paypal/i).click()
|
||||
})
|
||||
|
||||
it('should not render card element', function () {
|
||||
cy.get('@form').within(() => {
|
||||
cy.findByLabelText(/card details/i).should('not.exist')
|
||||
})
|
||||
})
|
||||
|
||||
it('should not render first name input', function () {
|
||||
cy.get('@form').within(() => {
|
||||
cy.findByLabelText(/first name/i).should('not.exist')
|
||||
})
|
||||
})
|
||||
|
||||
it('should not render last name input', function () {
|
||||
cy.get('@form').within(() => {
|
||||
cy.findByLabelText(/last name/i).should('not.exist')
|
||||
})
|
||||
})
|
||||
|
||||
it('renders proceeding to PayPal notice', function () {
|
||||
cy.get('@form').within(() => {
|
||||
cy.contains(
|
||||
/proceeding to PayPal will take you to the PayPal site to pay for your subscription/i
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
it('handles the disabled state of submit button', function () {
|
||||
cy.get('@form').within(() => {
|
||||
cy.findByRole('button', { name: /proceed to paypal/i }).should(
|
||||
'be.disabled'
|
||||
)
|
||||
cy.findByLabelText(/country/i).select('Bulgaria')
|
||||
cy.findByRole('button', { name: /proceed to paypal/i }).should(
|
||||
'not.be.disabled'
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
|
@ -0,0 +1,42 @@
|
|||
import {
|
||||
createFakeRecurly,
|
||||
defaultSubscription,
|
||||
} from '../../fixtures/recurly-mock'
|
||||
import { PaymentProvider } from '../../../../../../frontend/js/features/subscription/context/payment-context'
|
||||
import { plans } from '../../fixtures/plans'
|
||||
|
||||
describe('common recurly validations', function () {
|
||||
beforeEach(function () {
|
||||
const plan = plans.find(({ planCode }) => planCode === 'collaborator')
|
||||
|
||||
if (!plan) {
|
||||
throw new Error('No plan was found while running the test!')
|
||||
}
|
||||
|
||||
cy.window().then(win => {
|
||||
win.metaAttributesCache = new Map()
|
||||
win.metaAttributesCache.set('ol-countryCode', '')
|
||||
win.metaAttributesCache.set('ol-recurlyApiKey', '1234')
|
||||
win.metaAttributesCache.set('ol-recommendedCurrency', 'USD')
|
||||
win.metaAttributesCache.set('ol-plan', plan)
|
||||
win.metaAttributesCache.set('ol-planCode', plan.planCode)
|
||||
win.metaAttributesCache.set('ol-showCouponField', true)
|
||||
win.recurly = createFakeRecurly(defaultSubscription)
|
||||
cy.interceptEvents()
|
||||
})
|
||||
})
|
||||
|
||||
it('initializes recurly', function () {
|
||||
cy.window().then(win => {
|
||||
cy.spy(win.recurly, 'configure')
|
||||
cy.spy(win.recurly.Pricing, 'Subscription')
|
||||
})
|
||||
|
||||
cy.mount(<PaymentProvider publicKey="0000" />)
|
||||
|
||||
cy.window().then(win => {
|
||||
expect(win.recurly.configure).to.be.calledOnce
|
||||
expect(win.recurly.Pricing.Subscription).to.be.calledOnce
|
||||
})
|
||||
})
|
||||
})
|
|
@ -0,0 +1,310 @@
|
|||
import PaymentPreviewPanel from '../../../../../../frontend/js/features/subscription/components/new/payment-preview/payment-preview-panel'
|
||||
import { PaymentProvider } from '../../../../../../frontend/js/features/subscription/context/payment-context'
|
||||
import { plans } from '../../fixtures/plans'
|
||||
import {
|
||||
createFakeRecurly,
|
||||
defaultSubscription,
|
||||
} from '../../fixtures/recurly-mock'
|
||||
import { cloneDeep } from 'lodash'
|
||||
import { Plan } from '../../../../../../types/subscription/plan'
|
||||
|
||||
function PaymentPreviewPanelWithPaymentProvider() {
|
||||
return (
|
||||
<PaymentProvider publicKey="0000">
|
||||
<PaymentPreviewPanel />
|
||||
</PaymentProvider>
|
||||
)
|
||||
}
|
||||
|
||||
describe('payment preview panel', function () {
|
||||
beforeEach(function () {
|
||||
const plan = plans.find(({ planCode }) => planCode === 'collaborator')
|
||||
|
||||
if (!plan) {
|
||||
throw new Error('No plan was found while running the test!')
|
||||
}
|
||||
|
||||
cy.window().then(win => {
|
||||
win.metaAttributesCache = new Map()
|
||||
win.metaAttributesCache.set('ol-countryCode', '')
|
||||
win.metaAttributesCache.set('ol-recurlyApiKey', '0000')
|
||||
win.metaAttributesCache.set('ol-recommendedCurrency', 'USD')
|
||||
win.metaAttributesCache.set('ol-plan', plan)
|
||||
win.metaAttributesCache.set('ol-planCode', plan.planCode)
|
||||
cy.wrap(plan).as('plan')
|
||||
|
||||
// init default recurly
|
||||
win.recurly = createFakeRecurly(defaultSubscription)
|
||||
cy.interceptEvents()
|
||||
})
|
||||
})
|
||||
|
||||
it('renders plan name', function () {
|
||||
cy.mount(<PaymentPreviewPanelWithPaymentProvider />)
|
||||
|
||||
cy.contains(defaultSubscription.items.plan!.name)
|
||||
})
|
||||
|
||||
it('renders collaborators per project', function () {
|
||||
cy.mount(<PaymentPreviewPanelWithPaymentProvider />)
|
||||
|
||||
cy.get<Plan>('@plan').then(plan => {
|
||||
cy.contains(`${plan.features?.collaborators} collaborators per project`)
|
||||
})
|
||||
})
|
||||
|
||||
it('renders features list', function () {
|
||||
cy.mount(<PaymentPreviewPanelWithPaymentProvider />)
|
||||
|
||||
cy.contains(/all premium features/i)
|
||||
cy.findByTestId('features-list').within(() => {
|
||||
cy.get(':nth-child(1)').contains(/increased compile timeout/i)
|
||||
cy.get(':nth-child(2)').contains(/sync with dropbox and github/i)
|
||||
cy.get(':nth-child(3)').contains(/full document history/i)
|
||||
cy.get(':nth-child(4)').contains(/track changes/i)
|
||||
cy.get(':nth-child(5)').contains(/advanced reference search/i)
|
||||
cy.get(':nth-child(6)').contains(/reference manager sync/i)
|
||||
cy.get(':nth-child(7)').contains(/symbol palette/i)
|
||||
})
|
||||
})
|
||||
|
||||
it('renders no features list', function () {
|
||||
cy.window().then(win => {
|
||||
cy.get<Plan>('@plan').then(plan => {
|
||||
const { features: _, ...noFeaturesPlan } = plan
|
||||
win.metaAttributesCache.set('ol-plan', noFeaturesPlan)
|
||||
})
|
||||
})
|
||||
|
||||
cy.mount(<PaymentPreviewPanelWithPaymentProvider />)
|
||||
|
||||
cy.findByTestId('features-list').should('not.exist')
|
||||
})
|
||||
|
||||
describe('price summary', function () {
|
||||
beforeEach(function () {
|
||||
cy.mount(<PaymentPreviewPanelWithPaymentProvider />)
|
||||
cy.findByTestId('price-summary').as('priceSummary')
|
||||
cy.findByTestId('price-summary-plan').as('priceSummaryPlan')
|
||||
cy.findByTestId('price-summary-coupon').as('priceSummaryCoupon')
|
||||
cy.findByTestId('price-summary-vat').as('priceSummaryVat')
|
||||
cy.findByTestId('price-summary-total').as('priceSummaryTotal')
|
||||
})
|
||||
|
||||
it('renders title', function () {
|
||||
cy.get('@priceSummary').contains(/payment summary/i)
|
||||
})
|
||||
|
||||
it('renders plan info', function () {
|
||||
cy.get('@priceSummaryPlan').contains(defaultSubscription.items.plan!.name)
|
||||
cy.get('@priceSummaryPlan').contains(
|
||||
`$${defaultSubscription.price.base.plan.unit}`
|
||||
)
|
||||
})
|
||||
|
||||
it('renders coupon info', function () {
|
||||
cy.get('@priceSummaryCoupon').contains(
|
||||
defaultSubscription.items.coupon!.name
|
||||
)
|
||||
cy.get('@priceSummaryCoupon').contains(
|
||||
`Discount of $${defaultSubscription.price.now.discount}`
|
||||
)
|
||||
})
|
||||
|
||||
it('does not render coupon info when there is no coupon', function () {
|
||||
cy.window().then(win => {
|
||||
const { coupon: _, ...items } = defaultSubscription.items
|
||||
win.recurly = createFakeRecurly({ ...defaultSubscription, items })
|
||||
})
|
||||
cy.mount(<PaymentPreviewPanelWithPaymentProvider />)
|
||||
cy.findByTestId('price-summary-coupon').should('not.exist')
|
||||
})
|
||||
|
||||
it('renders VAT', function () {
|
||||
cy.get('@priceSummaryVat').contains(
|
||||
`VAT ${parseFloat(defaultSubscription.price.taxes[0].rate) * 100}%`
|
||||
)
|
||||
cy.get('@priceSummaryVat').contains(
|
||||
`$${defaultSubscription.price.now.tax}`
|
||||
)
|
||||
})
|
||||
|
||||
describe('total amount', function () {
|
||||
it('renders "total per month" text', function () {
|
||||
cy.window().then(win => {
|
||||
const clone = cloneDeep(defaultSubscription)
|
||||
clone.items.plan!.period.length = 1
|
||||
win.recurly = createFakeRecurly(clone)
|
||||
})
|
||||
cy.mount(<PaymentPreviewPanelWithPaymentProvider />)
|
||||
cy.findByTestId('price-summary-total').contains(/total per month/i)
|
||||
})
|
||||
|
||||
it('renders "total per year" text', function () {
|
||||
cy.window().then(win => {
|
||||
const clone = cloneDeep(defaultSubscription)
|
||||
clone.items.plan!.period.length = 2
|
||||
win.recurly = createFakeRecurly(clone)
|
||||
})
|
||||
cy.mount(<PaymentPreviewPanelWithPaymentProvider />)
|
||||
cy.findByTestId('price-summary-total').contains(/total per year/i)
|
||||
})
|
||||
|
||||
it('renders total amount', function () {
|
||||
cy.get('@priceSummaryTotal').contains(
|
||||
`$${defaultSubscription.price.now.total}`
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
it('renders "change currency" dropdown and changes currency', function () {
|
||||
cy.get('@priceSummary').within(() => {
|
||||
cy.get('@priceSummary')
|
||||
.findByRole('button', { name: /change currency/i })
|
||||
.as('button')
|
||||
cy.findByRole('menu').should('not.exist')
|
||||
cy.get('@button').click()
|
||||
cy.findByRole('menu').within(() => {
|
||||
cy.findByRole('menuitem', { name: /usd \(\$\)/i }).contains(
|
||||
/selected/i
|
||||
)
|
||||
cy.findByRole('menuitem', { name: /eur \(€\)/i })
|
||||
cy.findByRole('menuitem', { name: /gbp \(£\)/i }).click()
|
||||
|
||||
cy.get('@priceSummaryPlan').contains(
|
||||
`£${defaultSubscription.price.base.plan.unit}`
|
||||
)
|
||||
cy.get('@priceSummaryCoupon').contains(
|
||||
`Discount of £${defaultSubscription.price.now.discount}`
|
||||
)
|
||||
cy.get('@priceSummaryVat').contains(
|
||||
`£${defaultSubscription.price.now.tax}`
|
||||
)
|
||||
cy.get('@priceSummaryTotal').contains(
|
||||
`£${defaultSubscription.price.now.total}`
|
||||
)
|
||||
})
|
||||
})
|
||||
cy.findByTestId('trial-coupon-summary')
|
||||
.should('not.contain.text', '$')
|
||||
.should('contain.text', '£')
|
||||
})
|
||||
})
|
||||
|
||||
describe('trial coupon summary', function () {
|
||||
it('renders trial price', function () {
|
||||
cy.mount(<PaymentPreviewPanelWithPaymentProvider />)
|
||||
cy.findByTestId('trial-coupon-summary').contains(
|
||||
`First ${defaultSubscription.items.plan!.trial!.length} days free, ` +
|
||||
`after that $${defaultSubscription.price.now.total} per month`
|
||||
)
|
||||
})
|
||||
|
||||
it('renders "X price for Y months"', function () {
|
||||
cy.window()
|
||||
.then(win => {
|
||||
const clone = cloneDeep(defaultSubscription)
|
||||
clone.items.coupon!.applies_for_months = 6
|
||||
clone.items.coupon!.single_use = false
|
||||
clone.items.plan!.period.length = 1
|
||||
win.recurly = createFakeRecurly(clone)
|
||||
return clone
|
||||
})
|
||||
.then((clone: typeof defaultSubscription) => {
|
||||
cy.mount(<PaymentPreviewPanelWithPaymentProvider />)
|
||||
cy.findByTestId('trial-coupon-summary').contains(
|
||||
`$${clone.price.now.total} for your first ${
|
||||
clone.items.coupon!.applies_for_months
|
||||
} months`
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
it('renders "X price for first month"', function () {
|
||||
cy.window()
|
||||
.then(win => {
|
||||
const clone = cloneDeep(defaultSubscription)
|
||||
clone.items.plan!.period.length = 1
|
||||
win.recurly = createFakeRecurly(clone)
|
||||
})
|
||||
.then(() => {
|
||||
cy.mount(<PaymentPreviewPanelWithPaymentProvider />)
|
||||
cy.findByTestId('trial-coupon-summary').contains(
|
||||
`$${defaultSubscription.price.now.total} for your first month`
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
it('renders "X price for first year"', function () {
|
||||
cy.mount(<PaymentPreviewPanelWithPaymentProvider />)
|
||||
cy.findByTestId('trial-coupon-summary').contains(
|
||||
`$${defaultSubscription.price.now.total} for your first year`
|
||||
)
|
||||
})
|
||||
|
||||
it('renders "then X price per month"', function () {
|
||||
cy.window()
|
||||
.then(win => {
|
||||
const clone = cloneDeep(defaultSubscription)
|
||||
clone.items.coupon!.applies_for_months = 6
|
||||
clone.items.coupon!.single_use = false
|
||||
clone.items.plan!.period.length = 1
|
||||
win.recurly = createFakeRecurly(clone)
|
||||
})
|
||||
.then(() => {
|
||||
cy.mount(<PaymentPreviewPanelWithPaymentProvider />)
|
||||
cy.findByTestId('trial-coupon-summary').contains(
|
||||
`Then $26.00 per month`
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
it('renders "then X price per year"', function () {
|
||||
cy.mount(<PaymentPreviewPanelWithPaymentProvider />)
|
||||
cy.findByTestId('trial-coupon-summary').contains(`Then $26.00 per year`)
|
||||
})
|
||||
|
||||
it('renders "normally X price per month"', function () {
|
||||
cy.window()
|
||||
.then(win => {
|
||||
const clone = cloneDeep(defaultSubscription)
|
||||
clone.items.coupon!.applies_for_months = 0
|
||||
clone.items.coupon!.single_use = false
|
||||
clone.items.plan!.period.length = 1
|
||||
win.recurly = createFakeRecurly(clone)
|
||||
})
|
||||
.then(() => {
|
||||
cy.mount(<PaymentPreviewPanelWithPaymentProvider />)
|
||||
cy.findByTestId('trial-coupon-summary').contains(
|
||||
`Normally $26.00 per month`
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
it('renders "normally X price per year"', function () {
|
||||
cy.window()
|
||||
.then(win => {
|
||||
const clone = cloneDeep(defaultSubscription)
|
||||
clone.items.coupon!.single_use = false
|
||||
clone.items.plan!.period.length = 2
|
||||
win.recurly = createFakeRecurly(clone)
|
||||
})
|
||||
.then(() => {
|
||||
cy.mount(<PaymentPreviewPanelWithPaymentProvider />)
|
||||
cy.findByTestId('trial-coupon-summary').contains(
|
||||
`Normally $26.00 per year`
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('renders "cancel anytime" content', function () {
|
||||
cy.mount(<PaymentPreviewPanelWithPaymentProvider />)
|
||||
|
||||
cy.contains(
|
||||
/we’re confident that you’ll love Overleaf, but if not you can cancel anytime/i
|
||||
).contains(
|
||||
/we’ll give you your money back, no questions asked, if you let us know within 30 days/i
|
||||
)
|
||||
})
|
||||
})
|
|
@ -0,0 +1,230 @@
|
|||
import {
|
||||
Address,
|
||||
CardElementOptions,
|
||||
ElementsInstance,
|
||||
PayPalConfig,
|
||||
PlanOptions,
|
||||
Recurly,
|
||||
RecurlyError,
|
||||
RecurlyOptions,
|
||||
RiskOptions,
|
||||
Tax,
|
||||
TokenPayload,
|
||||
} from 'recurly__recurly-js'
|
||||
import { SubscriptionPricingInstanceCustom } from '../../../../../types/recurly/pricing/subscription'
|
||||
|
||||
export const defaultSubscription = {
|
||||
id: '123',
|
||||
price: {
|
||||
base: {
|
||||
plan: {
|
||||
unit: '20.00',
|
||||
setup_fee: '0.00',
|
||||
},
|
||||
},
|
||||
next: {
|
||||
addons: '0.00',
|
||||
discount: '1.00',
|
||||
plan: '10.00',
|
||||
setup_fee: '0.00',
|
||||
subtotal: '9.00',
|
||||
tax: '1.20',
|
||||
total: '12.00',
|
||||
},
|
||||
now: {
|
||||
addons: '0.00',
|
||||
discount: '1.00',
|
||||
plan: '10.00',
|
||||
setup_fee: '0.00',
|
||||
subtotal: '9.00',
|
||||
tax: '1.20',
|
||||
total: '12.00',
|
||||
},
|
||||
taxes: [
|
||||
{
|
||||
tax_type: 'tax_type_1',
|
||||
region: 'EU',
|
||||
rate: '0.3',
|
||||
},
|
||||
],
|
||||
},
|
||||
items: {
|
||||
coupon: {
|
||||
applies_for_months: 2,
|
||||
code: 'react',
|
||||
discount: {
|
||||
type: 'percent',
|
||||
rate: 0.2,
|
||||
},
|
||||
name: 'fake coupon',
|
||||
single_use: true,
|
||||
},
|
||||
currency: 'USD',
|
||||
plan: {
|
||||
code: 'asd',
|
||||
name: 'Standard (Collaborator)',
|
||||
period: {
|
||||
interval: '2',
|
||||
length: 5,
|
||||
},
|
||||
price: {
|
||||
'15': {
|
||||
unit_amount: 5,
|
||||
symbol: '$',
|
||||
setup_fee: 2,
|
||||
},
|
||||
},
|
||||
quantity: 1,
|
||||
tax_code: 'digital',
|
||||
tax_exempt: false,
|
||||
trial: {
|
||||
interval: 'weekly',
|
||||
length: 7,
|
||||
},
|
||||
},
|
||||
},
|
||||
} as unknown as SubscriptionPricingInstanceCustom
|
||||
|
||||
class PayPalBase {
|
||||
protected constructor(config?: PayPalConfig) {
|
||||
Object.assign(this, config)
|
||||
}
|
||||
|
||||
static PayPal = () => new this()
|
||||
|
||||
destroy() {}
|
||||
|
||||
on(_eventName: string, _callback: () => void) {}
|
||||
}
|
||||
|
||||
class Card {
|
||||
protected fakeCardEl
|
||||
|
||||
constructor() {
|
||||
this.fakeCardEl = document.createElement('div')
|
||||
}
|
||||
|
||||
attach(el: HTMLElement) {
|
||||
this.fakeCardEl.dataset.testid = 'test-card-element'
|
||||
const input = document.createElement('input')
|
||||
input.style.border = '1px solid black'
|
||||
input.style.width = '50px'
|
||||
const cardNumberInput = input.cloneNode(true) as HTMLInputElement
|
||||
cardNumberInput.style.width = '200px'
|
||||
cardNumberInput.placeholder = 'XXXX-XXXX-XXXX-XXXX'
|
||||
|
||||
this.fakeCardEl.appendChild(cardNumberInput)
|
||||
this.fakeCardEl.appendChild(input.cloneNode(true))
|
||||
this.fakeCardEl.appendChild(input.cloneNode(true))
|
||||
this.fakeCardEl.appendChild(input.cloneNode(true))
|
||||
el.appendChild(this.fakeCardEl)
|
||||
}
|
||||
|
||||
on(eventName = 'change', callback: (state: Record<string, unknown>) => void) {
|
||||
this.fakeCardEl.querySelectorAll('input').forEach(node => {
|
||||
node.addEventListener(eventName, () => {
|
||||
const state = {
|
||||
valid: true,
|
||||
}
|
||||
callback(state)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export class ElementsBase {
|
||||
protected constructor(config?: unknown) {
|
||||
Object.assign(this, config)
|
||||
}
|
||||
|
||||
static Elements = () => new this()
|
||||
|
||||
CardElement(_cardElementOptions?: CardElementOptions) {
|
||||
return new Card()
|
||||
}
|
||||
}
|
||||
|
||||
export class ThreeDSecureBase {
|
||||
protected constructor(riskOptions?: RiskOptions) {
|
||||
Object.assign(this, riskOptions)
|
||||
}
|
||||
|
||||
static ThreeDSecure = (_riskOptions: RiskOptions) => new this()
|
||||
|
||||
on(_eventName = 'change', _callback: () => void) {}
|
||||
|
||||
attach(el: HTMLElement) {
|
||||
el.textContent = '3D challenge content'
|
||||
}
|
||||
}
|
||||
|
||||
abstract class PricingBase {
|
||||
plan(_planCode: string, _options: PlanOptions) {
|
||||
return this
|
||||
}
|
||||
|
||||
address(_address: Address) {
|
||||
return this
|
||||
}
|
||||
|
||||
tax(_tax: Tax) {
|
||||
return this
|
||||
}
|
||||
|
||||
currency(_currency: string) {
|
||||
return this
|
||||
}
|
||||
|
||||
coupon(_coupon: string) {
|
||||
return this
|
||||
}
|
||||
|
||||
catch(_callback?: (reason?: RecurlyError) => void) {
|
||||
return this
|
||||
}
|
||||
|
||||
done(callback?: () => unknown) {
|
||||
callback?.()
|
||||
return this
|
||||
}
|
||||
|
||||
on(_eventName: string, callback: () => void) {
|
||||
callback()
|
||||
}
|
||||
}
|
||||
|
||||
const createSubscriptionClass = (classProps: unknown) => {
|
||||
return class extends PricingBase {
|
||||
protected constructor() {
|
||||
super()
|
||||
Object.assign(this, classProps)
|
||||
}
|
||||
|
||||
static Subscription = () => new this()
|
||||
}
|
||||
}
|
||||
|
||||
// Using `overrides` as currently can't stub/spy external files with cypress
|
||||
export const createFakeRecurly = (classProps: unknown, overrides = {}) => {
|
||||
return {
|
||||
configure: (_options: RecurlyOptions) => {},
|
||||
token: (
|
||||
_elements: ElementsInstance,
|
||||
_second: unknown,
|
||||
handler: (
|
||||
err?: RecurlyError | null,
|
||||
recurlyBillingToken?: TokenPayload,
|
||||
threeDResultToken?: TokenPayload
|
||||
) => void
|
||||
) => {
|
||||
handler(undefined, undefined, { id: '123', type: '456' })
|
||||
},
|
||||
Elements: ElementsBase.Elements,
|
||||
PayPal: PayPalBase.PayPal,
|
||||
Pricing: createSubscriptionClass(classProps),
|
||||
Risk: () => ({
|
||||
ThreeDSecure: ThreeDSecureBase.ThreeDSecure,
|
||||
}),
|
||||
...overrides,
|
||||
} as unknown as Recurly
|
||||
}
|
Loading…
Add table
Reference in a new issue