Merge pull request #11872 from overleaf/ii-payment-page-tests

[web] Payment page tests

GitOrigin-RevId: 0ab9a75c13f1833cbdf7aa71ffe3ab66174ca773
This commit is contained in:
ilkin-overleaf 2023-02-28 12:04:44 +02:00 committed by Copybot
parent 612728d300
commit 076bc9b39c
13 changed files with 1073 additions and 11 deletions

View file

@ -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">

View file

@ -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}>

View file

@ -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})

View file

@ -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>
)}

View file

@ -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>
&ndash;{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}

View file

@ -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>
</>
)
}

View file

@ -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) {

View file

@ -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 youre 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'
)
})
})
})
})

View file

@ -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
})
})
})

View file

@ -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(
/were confident that youll love Overleaf, but if not you can cancel anytime/i
).contains(
/well give you your money back, no questions asked, if you let us know within 30 days/i
)
})
})

View file

@ -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
}