Просмотр исходного кода

Merge pull request #11602 from overleaf/jel-react-personal-subscription-dash-pt-6

[web] Continue migration of change plan UI to React

GitOrigin-RevId: 4f6176ac101ba3c57134cf5c86329e205f17ab23
Jessica Lawshe 3 лет назад
Родитель
Сommit
d4057a7bcc

+ 15 - 0
services/web/frontend/js/features/subscription/data/currency.ts

@@ -0,0 +1,15 @@
+export const currencies = <const>{
+  USD: '$',
+  EUR: '€',
+  GBP: '£',
+  SEK: 'kr',
+  CAD: '$',
+  NOK: 'kr',
+  DKK: 'kr',
+  AUD: '$',
+  NZD: '$',
+  CHF: 'Fr',
+  SGD: '$',
+}
+
+export type CurrencyCode = keyof typeof currencies

+ 56 - 0
services/web/frontend/js/features/subscription/util/recurly-pricing.tsx

@@ -0,0 +1,56 @@
+import { SubscriptionPricingState } from '@recurly/recurly-js'
+import getMeta from '../../../utils/meta'
+import { currencies, CurrencyCode } from '../data/currency'
+
+export function formatPriceForDisplayData(price: string, taxRate: number) {
+  const currencyCode: CurrencyCode = getMeta('ol-recommendedCurrency')
+  const currencySymbol = currencies[currencyCode]
+
+  const totalPriceExTax = parseFloat(price)
+  let taxAmount = totalPriceExTax * taxRate
+  if (isNaN(taxAmount)) {
+    taxAmount = 0
+  }
+  const totalWithTax = totalPriceExTax + taxAmount
+
+  return {
+    totalForDisplay: `${currencySymbol}${
+      totalWithTax % 1 !== 0 ? totalWithTax.toFixed(2) : totalWithTax
+    }`,
+    totalAsNumber: totalWithTax,
+    subtotal: `${currencySymbol}${totalPriceExTax.toFixed(2)}`,
+    tax: `${currencySymbol}${taxAmount.toFixed(2)}`,
+    includesTax: taxAmount !== 0,
+  }
+}
+
+export function loadDisplayPriceWithTaxPromise(
+  planCode: string,
+  currency: CurrencyCode,
+  taxRate: number
+) {
+  if (!recurly) return
+
+  return new Promise<ReturnType<typeof formatPriceForDisplayData> | undefined>(
+    resolve => {
+      recurly.Pricing.Subscription()
+        .plan(planCode, { quantity: 1 })
+        .currency(currency)
+        .catch(function (error) {
+          console.error(error)
+        })
+        .done(response => {
+          if (response) {
+            const price =
+              response as unknown as SubscriptionPricingState['price']
+            // type expects response to be {price: {next: ...}}
+            // but the real response is {next: ...}
+            const data = formatPriceForDisplayData(price.next.total, taxRate)
+            resolve(data)
+          } else {
+            resolve(undefined)
+          }
+        })
+    }
+  )
+}

+ 17 - 32
services/web/test/frontend/features/subscription/components/dashboard/institution-memberships.test.tsx

@@ -1,7 +1,10 @@
 import { expect } from 'chai'
 import { expect } from 'chai'
-import { render, screen } from '@testing-library/react'
+import { screen } from '@testing-library/react'
 import InstitutionMemberships from '../../../../../../frontend/js/features/subscription/components/dashboard/institution-memberships'
 import InstitutionMemberships from '../../../../../../frontend/js/features/subscription/components/dashboard/institution-memberships'
-import { SubscriptionDashboardProvider } from '../../../../../../frontend/js/features/subscription/context/subscription-dashboard-context'
+import {
+  cleanUpContext,
+  renderWithSubscriptionDashContext,
+} from '../../helpers/render-with-subscription-dash-context'
 
 
 const memberships = [
 const memberships = [
   {
   {
@@ -27,25 +30,16 @@ const memberships = [
 ]
 ]
 
 
 describe('<InstitutionMemberships />', function () {
 describe('<InstitutionMemberships />', function () {
-  beforeEach(function () {
-    window.metaAttributesCache = new Map()
-  })
-
   afterEach(function () {
   afterEach(function () {
-    window.metaAttributesCache = new Map()
+    cleanUpContext()
   })
   })
 
 
   it('renders all insitutions with license', function () {
   it('renders all insitutions with license', function () {
-    window.metaAttributesCache.set(
-      'ol-currentInstitutionsWithLicence',
-      memberships
-    )
-
-    render(
-      <SubscriptionDashboardProvider>
-        <InstitutionMemberships />
-      </SubscriptionDashboardProvider>
-    )
+    renderWithSubscriptionDashContext(<InstitutionMemberships />, {
+      metaTags: [
+        { name: 'ol-currentInstitutionsWithLicence', value: memberships },
+      ],
+    })
 
 
     const elements = screen.getAllByText('You are on our', {
     const elements = screen.getAllByText('You are on our', {
       exact: false,
       exact: false,
@@ -60,27 +54,18 @@ describe('<InstitutionMemberships />', function () {
   })
   })
 
 
   it('renders error message when failed to check commons licenses', function () {
   it('renders error message when failed to check commons licenses', function () {
-    render(
-      <SubscriptionDashboardProvider>
-        <InstitutionMemberships />
-      </SubscriptionDashboardProvider>
-    )
+    renderWithSubscriptionDashContext(<InstitutionMemberships />)
     screen.getByText(
     screen.getByText(
       'Sorry, something went wrong. Subscription information related to institutional affiliations may not be displayed. Please try again later.'
       'Sorry, something went wrong. Subscription information related to institutional affiliations may not be displayed. Please try again later.'
     )
     )
   })
   })
 
 
   it('renders the "Get the most out of your" subscription text when a user has a subscription', function () {
   it('renders the "Get the most out of your" subscription text when a user has a subscription', function () {
-    window.metaAttributesCache.set(
-      'ol-currentInstitutionsWithLicence',
-      memberships
-    )
-
-    render(
-      <SubscriptionDashboardProvider>
-        <InstitutionMemberships />
-      </SubscriptionDashboardProvider>
-    )
+    renderWithSubscriptionDashContext(<InstitutionMemberships />, {
+      metaTags: [
+        { name: 'ol-currentInstitutionsWithLicence', value: memberships },
+      ],
+    })
     screen.getByText('Get the most out of your', {
     screen.getByText('Get the most out of your', {
       exact: false,
       exact: false,
     })
     })

+ 15 - 21
services/web/test/frontend/features/subscription/components/dashboard/managed-group-subscriptions.test.tsx

@@ -1,12 +1,15 @@
 import { expect } from 'chai'
 import { expect } from 'chai'
-import { render, screen } from '@testing-library/react'
+import { screen } from '@testing-library/react'
 import {
 import {
   groupActiveSubscription,
   groupActiveSubscription,
   groupActiveSubscriptionWithPendingLicenseChange,
   groupActiveSubscriptionWithPendingLicenseChange,
 } from '../../fixtures/subscriptions'
 } from '../../fixtures/subscriptions'
 import ManagedGroupSubscriptions from '../../../../../../frontend/js/features/subscription/components/dashboard/managed-group-subscriptions'
 import ManagedGroupSubscriptions from '../../../../../../frontend/js/features/subscription/components/dashboard/managed-group-subscriptions'
 import { ManagedGroupSubscription } from '../../../../../../types/subscription/dashboard/subscription'
 import { ManagedGroupSubscription } from '../../../../../../types/subscription/dashboard/subscription'
-import { SubscriptionDashboardProvider } from '../../../../../../frontend/js/features/subscription/context/subscription-dashboard-context'
+import {
+  cleanUpContext,
+  renderWithSubscriptionDashContext,
+} from '../../helpers/render-with-subscription-dash-context'
 
 
 const managedGroupSubscriptions: ManagedGroupSubscription[] = [
 const managedGroupSubscriptions: ManagedGroupSubscription[] = [
   {
   {
@@ -30,24 +33,19 @@ const managedGroupSubscriptions: ManagedGroupSubscription[] = [
 ]
 ]
 
 
 describe('<ManagedGroupSubscriptions />', function () {
 describe('<ManagedGroupSubscriptions />', function () {
-  beforeEach(function () {
-    window.metaAttributesCache = new Map()
-  })
-
   afterEach(function () {
   afterEach(function () {
-    window.metaAttributesCache = new Map()
+    cleanUpContext()
   })
   })
 
 
   it('renders all managed group subscriptions', function () {
   it('renders all managed group subscriptions', function () {
-    window.metaAttributesCache.set(
-      'ol-managedGroupSubscriptions',
-      managedGroupSubscriptions
-    )
-    render(
-      <SubscriptionDashboardProvider>
-        <ManagedGroupSubscriptions />
-      </SubscriptionDashboardProvider>
-    )
+    renderWithSubscriptionDashContext(<ManagedGroupSubscriptions />, {
+      metaTags: [
+        {
+          name: 'ol-managedGroupSubscriptions',
+          value: managedGroupSubscriptions,
+        },
+      ],
+    })
 
 
     const elements = screen.getAllByText('You are a', {
     const elements = screen.getAllByText('You are a', {
       exact: false,
       exact: false,
@@ -91,11 +89,7 @@ describe('<ManagedGroupSubscriptions />', function () {
   })
   })
 
 
   it('renders nothing when there are no group memberships', function () {
   it('renders nothing when there are no group memberships', function () {
-    render(
-      <SubscriptionDashboardProvider>
-        <ManagedGroupSubscriptions />
-      </SubscriptionDashboardProvider>
-    )
+    renderWithSubscriptionDashContext(<ManagedGroupSubscriptions />)
     const elements = screen.queryAllByText('You are a', {
     const elements = screen.queryAllByText('You are a', {
       exact: false,
       exact: false,
     })
     })

+ 40 - 74
services/web/test/frontend/features/subscription/components/dashboard/personal-subscription.test.tsx

@@ -1,32 +1,25 @@
 import { expect } from 'chai'
 import { expect } from 'chai'
-import { render, screen } from '@testing-library/react'
+import { screen } from '@testing-library/react'
 import PersonalSubscription from '../../../../../../frontend/js/features/subscription/components/dashboard/personal-subscription'
 import PersonalSubscription from '../../../../../../frontend/js/features/subscription/components/dashboard/personal-subscription'
-import { SubscriptionDashboardProvider } from '../../../../../../frontend/js/features/subscription/context/subscription-dashboard-context'
 import {
 import {
   annualActiveSubscription,
   annualActiveSubscription,
   canceledSubscription,
   canceledSubscription,
   pastDueExpiredSubscription,
   pastDueExpiredSubscription,
 } from '../../fixtures/subscriptions'
 } from '../../fixtures/subscriptions'
+import {
+  cleanUpContext,
+  renderWithSubscriptionDashContext,
+} from '../../helpers/render-with-subscription-dash-context'
 
 
 describe('<PersonalSubscription />', function () {
 describe('<PersonalSubscription />', function () {
-  beforeEach(function () {
-    window.metaAttributesCache = new Map()
-    // @ts-ignore
-    window.recurly = {}
-  })
-
   afterEach(function () {
   afterEach(function () {
-    window.metaAttributesCache = new Map()
-    // @ts-ignore
-    delete window.recurly
+    cleanUpContext()
   })
   })
 
 
   describe('no subscription', function () {
   describe('no subscription', function () {
     it('returns empty container', function () {
     it('returns empty container', function () {
-      const { container } = render(
-        <SubscriptionDashboardProvider>
-          <PersonalSubscription />
-        </SubscriptionDashboardProvider>
+      const { container } = renderWithSubscriptionDashContext(
+        <PersonalSubscription />
       )
       )
       expect(container.firstChild).to.be.null
       expect(container.firstChild).to.be.null
     })
     })
@@ -34,26 +27,19 @@ describe('<PersonalSubscription />', function () {
 
 
   describe('subscription states  ', function () {
   describe('subscription states  ', function () {
     it('renders the active dash', function () {
     it('renders the active dash', function () {
-      window.metaAttributesCache.set(
-        'ol-subscription',
-        annualActiveSubscription
-      )
-      render(
-        <SubscriptionDashboardProvider>
-          <PersonalSubscription />
-        </SubscriptionDashboardProvider>
-      )
+      renderWithSubscriptionDashContext(<PersonalSubscription />, {
+        metaTags: [
+          { name: 'ol-subscription', value: annualActiveSubscription },
+        ],
+      })
 
 
       screen.getByText('You are currently subscribed to the', { exact: false })
       screen.getByText('You are currently subscribed to the', { exact: false })
     })
     })
 
 
     it('renders the canceled dash', function () {
     it('renders the canceled dash', function () {
-      window.metaAttributesCache.set('ol-subscription', canceledSubscription)
-      render(
-        <SubscriptionDashboardProvider>
-          <PersonalSubscription />
-        </SubscriptionDashboardProvider>
-      )
+      renderWithSubscriptionDashContext(<PersonalSubscription />, {
+        metaTags: [{ name: 'ol-subscription', value: canceledSubscription }],
+      })
       screen.getByText(
       screen.getByText(
         'Your subscription has been canceled and will terminate on',
         'Your subscription has been canceled and will terminate on',
         { exact: false }
         { exact: false }
@@ -74,15 +60,11 @@ describe('<PersonalSubscription />', function () {
     })
     })
 
 
     it('renders the expired dash', function () {
     it('renders the expired dash', function () {
-      window.metaAttributesCache.set(
-        'ol-subscription',
-        pastDueExpiredSubscription
-      )
-      render(
-        <SubscriptionDashboardProvider>
-          <PersonalSubscription />
-        </SubscriptionDashboardProvider>
-      )
+      renderWithSubscriptionDashContext(<PersonalSubscription />, {
+        metaTags: [
+          { name: 'ol-subscription', value: pastDueExpiredSubscription },
+        ],
+      })
       screen.getByText('Your subscription has expired.')
       screen.getByText('Your subscription has expired.')
     })
     })
 
 
@@ -92,12 +74,9 @@ describe('<PersonalSubscription />', function () {
         JSON.parse(JSON.stringify(annualActiveSubscription))
         JSON.parse(JSON.stringify(annualActiveSubscription))
       )
       )
       withStateDeleted.recurly.state = undefined
       withStateDeleted.recurly.state = undefined
-      window.metaAttributesCache.set('ol-subscription', withStateDeleted)
-      render(
-        <SubscriptionDashboardProvider>
-          <PersonalSubscription />
-        </SubscriptionDashboardProvider>
-      )
+      renderWithSubscriptionDashContext(<PersonalSubscription />, {
+        metaTags: [{ name: 'ol-subscription', value: withStateDeleted }],
+      })
       screen.getByText(
       screen.getByText(
         'There is a problem with your subscription. Please contact us for more information.'
         'There is a problem with your subscription. Please contact us for more information.'
       )
       )
@@ -106,15 +85,11 @@ describe('<PersonalSubscription />', function () {
 
 
   describe('past due subscription', function () {
   describe('past due subscription', function () {
     it('renders error alert', function () {
     it('renders error alert', function () {
-      window.metaAttributesCache.set(
-        'ol-subscription',
-        pastDueExpiredSubscription
-      )
-      render(
-        <SubscriptionDashboardProvider>
-          <PersonalSubscription />
-        </SubscriptionDashboardProvider>
-      )
+      renderWithSubscriptionDashContext(<PersonalSubscription />, {
+        metaTags: [
+          { name: 'ol-subscription', value: pastDueExpiredSubscription },
+        ],
+      })
       screen.getByRole('alert')
       screen.getByRole('alert')
       screen.getByText(
       screen.getByText(
         'Your account currently has a past due invoice. You will not be able to change your plan until this is resolved.',
         'Your account currently has a past due invoice. You will not be able to change your plan until this is resolved.',
@@ -132,17 +107,12 @@ describe('<PersonalSubscription />', function () {
       'Sorry, there was an error talking to our payment provider. Please try again in a few moments. If you are using any ad or script blocking extensions in your browser, you may need to temporarily disable them.'
       'Sorry, there was an error talking to our payment provider. Please try again in a few moments. If you are using any ad or script blocking extensions in your browser, you may need to temporarily disable them.'
 
 
     it('shows an alert and hides "Change plan" option when Recurly did not load', function () {
     it('shows an alert and hides "Change plan" option when Recurly did not load', function () {
-      // @ts-ignore
-      delete window.recurly
-      window.metaAttributesCache.set(
-        'ol-subscription',
-        annualActiveSubscription
-      )
-      render(
-        <SubscriptionDashboardProvider>
-          <PersonalSubscription />
-        </SubscriptionDashboardProvider>
-      )
+      renderWithSubscriptionDashContext(<PersonalSubscription />, {
+        metaTags: [
+          { name: 'ol-subscription', value: annualActiveSubscription },
+        ],
+        recurlyNotLoaded: true,
+      })
 
 
       screen.getByRole('alert')
       screen.getByRole('alert')
       screen.getByText(recurlyFailedToLoadText)
       screen.getByText(recurlyFailedToLoadText)
@@ -151,15 +121,11 @@ describe('<PersonalSubscription />', function () {
     })
     })
 
 
     it('should not show an alert and should show "Change plan" option when Recurly did load', function () {
     it('should not show an alert and should show "Change plan" option when Recurly did load', function () {
-      window.metaAttributesCache.set(
-        'ol-subscription',
-        annualActiveSubscription
-      )
-      render(
-        <SubscriptionDashboardProvider>
-          <PersonalSubscription />
-        </SubscriptionDashboardProvider>
-      )
+      renderWithSubscriptionDashContext(<PersonalSubscription />, {
+        metaTags: [
+          { name: 'ol-subscription', value: annualActiveSubscription },
+        ],
+      })
 
 
       expect(screen.queryByRole('alert')).to.be.null
       expect(screen.queryByRole('alert')).to.be.null
 
 

+ 29 - 61
services/web/test/frontend/features/subscription/components/dashboard/states/active/active.test.tsx

@@ -1,8 +1,7 @@
 import { expect } from 'chai'
 import { expect } from 'chai'
-import { fireEvent, render, screen } from '@testing-library/react'
+import { fireEvent, screen } from '@testing-library/react'
 import * as eventTracking from '../../../../../../../../frontend/js/infrastructure/event-tracking'
 import * as eventTracking from '../../../../../../../../frontend/js/infrastructure/event-tracking'
 import { ActiveSubscription } from '../../../../../../../../frontend/js/features/subscription/components/dashboard/states/active/active'
 import { ActiveSubscription } from '../../../../../../../../frontend/js/features/subscription/components/dashboard/states/active/active'
-import { SubscriptionDashboardProvider } from '../../../../../../../../frontend/js/features/subscription/context/subscription-dashboard-context'
 import { Subscription } from '../../../../../../../../types/subscription/dashboard/subscription'
 import { Subscription } from '../../../../../../../../types/subscription/dashboard/subscription'
 import {
 import {
   annualActiveSubscription,
   annualActiveSubscription,
@@ -13,22 +12,20 @@ import {
 } from '../../../../fixtures/subscriptions'
 } from '../../../../fixtures/subscriptions'
 import sinon from 'sinon'
 import sinon from 'sinon'
 import { plans } from '../../../../fixtures/plans'
 import { plans } from '../../../../fixtures/plans'
+import {
+  cleanUpContext,
+  renderWithSubscriptionDashContext,
+} from '../../../../helpers/render-with-subscription-dash-context'
 
 
 describe('<ActiveSubscription />', function () {
 describe('<ActiveSubscription />', function () {
   let sendMBSpy: sinon.SinonSpy
   let sendMBSpy: sinon.SinonSpy
 
 
   beforeEach(function () {
   beforeEach(function () {
-    window.metaAttributesCache = new Map()
-    window.metaAttributesCache.set('ol-plans', plans)
-    // @ts-ignore
-    window.recurly = {}
     sendMBSpy = sinon.spy(eventTracking, 'sendMB')
     sendMBSpy = sinon.spy(eventTracking, 'sendMB')
   })
   })
 
 
   afterEach(function () {
   afterEach(function () {
-    window.metaAttributesCache = new Map()
-    // @ts-ignore
-    delete window.recurly
+    cleanUpContext()
     sendMBSpy.restore()
     sendMBSpy.restore()
   })
   })
 
 
@@ -64,21 +61,26 @@ describe('<ActiveSubscription />', function () {
     screen.getByRole('link', { name: 'View Your Invoices' })
     screen.getByRole('link', { name: 'View Your Invoices' })
   }
   }
 
 
-  it('renders the dash annual active subscription', function () {
-    render(
-      <SubscriptionDashboardProvider>
-        <ActiveSubscription subscription={annualActiveSubscription} />
-      </SubscriptionDashboardProvider>
+  function renderActiveSubscription(subscription: Subscription) {
+    const renderOptions = {
+      metaTags: [
+        { name: 'ol-plans', value: plans },
+        { name: 'ol-subscription', value: subscription },
+      ],
+    }
+    renderWithSubscriptionDashContext(
+      <ActiveSubscription subscription={subscription} />,
+      renderOptions
     )
     )
+  }
+
+  it('renders the dash annual active subscription', function () {
+    renderActiveSubscription(annualActiveSubscription)
     expectedInActiveSubscription(annualActiveSubscription)
     expectedInActiveSubscription(annualActiveSubscription)
   })
   })
 
 
   it('shows change plan UI when button clicked', function () {
   it('shows change plan UI when button clicked', function () {
-    render(
-      <SubscriptionDashboardProvider>
-        <ActiveSubscription subscription={annualActiveSubscription} />
-      </SubscriptionDashboardProvider>
-    )
+    renderActiveSubscription(annualActiveSubscription)
 
 
     const button = screen.getByRole('button', { name: 'Change plan' })
     const button = screen.getByRole('button', { name: 'Change plan' })
     fireEvent.click(button)
     fireEvent.click(button)
@@ -93,11 +95,7 @@ describe('<ActiveSubscription />', function () {
   })
   })
 
 
   it('notes when user is changing plan at end of current plan term', function () {
   it('notes when user is changing plan at end of current plan term', function () {
-    render(
-      <SubscriptionDashboardProvider>
-        <ActiveSubscription subscription={pendingSubscriptionChange} />
-      </SubscriptionDashboardProvider>
-    )
+    renderActiveSubscription(pendingSubscriptionChange)
 
 
     expectedInActiveSubscription(pendingSubscriptionChange)
     expectedInActiveSubscription(pendingSubscriptionChange)
 
 
@@ -126,24 +124,14 @@ describe('<ActiveSubscription />', function () {
 
 
     activePastDueSubscription.recurly.account.has_past_due_invoice._ = 'true'
     activePastDueSubscription.recurly.account.has_past_due_invoice._ = 'true'
 
 
-    render(
-      <SubscriptionDashboardProvider>
-        <ActiveSubscription subscription={activePastDueSubscription} />
-      </SubscriptionDashboardProvider>
-    )
+    renderActiveSubscription(activePastDueSubscription)
 
 
     const changePlan = screen.queryByRole('button', { name: 'Change plan' })
     const changePlan = screen.queryByRole('button', { name: 'Change plan' })
     expect(changePlan).to.be.null
     expect(changePlan).to.be.null
   })
   })
 
 
   it('shows the pending license change message when plan change is pending', function () {
   it('shows the pending license change message when plan change is pending', function () {
-    render(
-      <SubscriptionDashboardProvider>
-        <ActiveSubscription
-          subscription={groupActiveSubscriptionWithPendingLicenseChange}
-        />
-      </SubscriptionDashboardProvider>
-    )
+    renderActiveSubscription(groupActiveSubscriptionWithPendingLicenseChange)
 
 
     screen.getByText('Your subscription is changing to include', {
     screen.getByText('Your subscription is changing to include', {
       exact: false,
       exact: false,
@@ -175,11 +163,7 @@ describe('<ActiveSubscription />', function () {
       subscription.recurly.totalLicenses +
       subscription.recurly.totalLicenses +
       subscription.recurly.additionalLicenses
       subscription.recurly.additionalLicenses
 
 
-    render(
-      <SubscriptionDashboardProvider>
-        <ActiveSubscription subscription={subscription} />
-      </SubscriptionDashboardProvider>
-    )
+    renderActiveSubscription(subscription)
 
 
     screen.getByText('Your subscription includes', {
     screen.getByText('Your subscription includes', {
       exact: false,
       exact: false,
@@ -193,11 +177,7 @@ describe('<ActiveSubscription />', function () {
   })
   })
 
 
   it('shows when trial ends and first payment collected', function () {
   it('shows when trial ends and first payment collected', function () {
-    render(
-      <SubscriptionDashboardProvider>
-        <ActiveSubscription subscription={trialSubscription} />
-      </SubscriptionDashboardProvider>
-    )
+    renderActiveSubscription(trialSubscription)
     screen.getByText('You’re on a free trial which ends on', { exact: false })
     screen.getByText('You’re on a free trial which ends on', { exact: false })
 
 
     const endDate = screen.getAllByText(
     const endDate = screen.getAllByText(
@@ -207,11 +187,7 @@ describe('<ActiveSubscription />', function () {
   })
   })
 
 
   it('shows cancel UI and sends event', function () {
   it('shows cancel UI and sends event', function () {
-    render(
-      <SubscriptionDashboardProvider>
-        <ActiveSubscription subscription={annualActiveSubscription} />
-      </SubscriptionDashboardProvider>
-    )
+    renderActiveSubscription(annualActiveSubscription)
     // before button clicked
     // before button clicked
     screen.getByText(
     screen.getByText(
       'Your subscription will remain active until the end of your billing period',
       'Your subscription will remain active until the end of your billing period',
@@ -238,22 +214,14 @@ describe('<ActiveSubscription />', function () {
 
 
   describe('group plans', function () {
   describe('group plans', function () {
     it('does not show "Change plan" option for group plans', function () {
     it('does not show "Change plan" option for group plans', function () {
-      render(
-        <SubscriptionDashboardProvider>
-          <ActiveSubscription subscription={groupActiveSubscription} />
-        </SubscriptionDashboardProvider>
-      )
+      renderActiveSubscription(groupActiveSubscription)
 
 
       const changePlan = screen.queryByRole('button', { name: 'Change plan' })
       const changePlan = screen.queryByRole('button', { name: 'Change plan' })
       expect(changePlan).to.be.null
       expect(changePlan).to.be.null
     })
     })
 
 
     it('shows contact support message for group plan change requests', function () {
     it('shows contact support message for group plan change requests', function () {
-      render(
-        <SubscriptionDashboardProvider>
-          <ActiveSubscription subscription={groupActiveSubscription} />
-        </SubscriptionDashboardProvider>
-      )
+      renderActiveSubscription(groupActiveSubscription)
       screen.getByRole('link', { name: 'contact support' })
       screen.getByRole('link', { name: 'contact support' })
       screen.getByText('if you wish to change your group subscription.', {
       screen.getByText('if you wish to change your group subscription.', {
         exact: false,
         exact: false,

+ 43 - 36
services/web/test/frontend/features/subscription/components/dashboard/states/active/change-plan/change-plan.test.tsx

@@ -1,6 +1,5 @@
 import { expect } from 'chai'
 import { expect } from 'chai'
-import { fireEvent, render, screen } from '@testing-library/react'
-import { SubscriptionDashboardProvider } from '../../../../../../../../../frontend/js/features/subscription/context/subscription-dashboard-context'
+import { fireEvent, screen } from '@testing-library/react'
 import { ChangePlan } from '../../../../../../../../../frontend/js/features/subscription/components/dashboard/states/active/change-plan/change-plan'
 import { ChangePlan } from '../../../../../../../../../frontend/js/features/subscription/components/dashboard/states/active/change-plan/change-plan'
 import { plans } from '../../../../../fixtures/plans'
 import { plans } from '../../../../../fixtures/plans'
 import {
 import {
@@ -8,38 +7,38 @@ import {
   pendingSubscriptionChange,
   pendingSubscriptionChange,
 } from '../../../../../fixtures/subscriptions'
 } from '../../../../../fixtures/subscriptions'
 import { ActiveSubscription } from '../../../../../../../../../frontend/js/features/subscription/components/dashboard/states/active/active'
 import { ActiveSubscription } from '../../../../../../../../../frontend/js/features/subscription/components/dashboard/states/active/active'
+import {
+  cleanUpContext,
+  renderWithSubscriptionDashContext,
+} from '../../../../../helpers/render-with-subscription-dash-context'
 
 
 describe('<ChangePlan />', function () {
 describe('<ChangePlan />', function () {
-  beforeEach(function () {
-    window.metaAttributesCache = new Map()
-    window.metaAttributesCache.set('ol-plans', plans)
-    // @ts-ignore
-    window.recurly = {}
-  })
+  const plansMetaTag = { name: 'ol-plans', value: plans }
+  const renderOptions = { metaTags: [plansMetaTag] }
 
 
   afterEach(function () {
   afterEach(function () {
-    window.metaAttributesCache = new Map()
-    // @ts-ignore
-    delete window.recurly
+    cleanUpContext()
   })
   })
 
 
   it('does not render the UI when showChangePersonalPlan is false', function () {
   it('does not render the UI when showChangePersonalPlan is false', function () {
     window.metaAttributesCache.delete('ol-plans')
     window.metaAttributesCache.delete('ol-plans')
-    const { container } = render(
-      <SubscriptionDashboardProvider>
-        <ChangePlan />
-      </SubscriptionDashboardProvider>
+    const { container } = renderWithSubscriptionDashContext(
+      <ChangePlan />,
+      renderOptions
     )
     )
 
 
     expect(container.firstChild).to.be.null
     expect(container.firstChild).to.be.null
   })
   })
 
 
   it('renders the individual plans table', function () {
   it('renders the individual plans table', function () {
-    window.metaAttributesCache.set('ol-subscription', annualActiveSubscription)
-    render(
-      <SubscriptionDashboardProvider>
-        <ActiveSubscription subscription={annualActiveSubscription} />
-      </SubscriptionDashboardProvider>
+    renderWithSubscriptionDashContext(
+      <ActiveSubscription subscription={annualActiveSubscription} />,
+      {
+        metaTags: [
+          { name: 'ol-subscription', value: annualActiveSubscription },
+          plansMetaTag,
+        ],
+      }
     )
     )
 
 
     const button = screen.getByRole('button', { name: 'Change plan' })
     const button = screen.getByRole('button', { name: 'Change plan' })
@@ -59,11 +58,14 @@ describe('<ChangePlan />', function () {
   })
   })
 
 
   it('renders the change to group plan UI', function () {
   it('renders the change to group plan UI', function () {
-    window.metaAttributesCache.set('ol-subscription', annualActiveSubscription)
-    render(
-      <SubscriptionDashboardProvider>
-        <ActiveSubscription subscription={annualActiveSubscription} />
-      </SubscriptionDashboardProvider>
+    renderWithSubscriptionDashContext(
+      <ActiveSubscription subscription={annualActiveSubscription} />,
+      {
+        metaTags: [
+          { name: 'ol-subscription', value: annualActiveSubscription },
+          plansMetaTag,
+        ],
+      }
     )
     )
 
 
     const button = screen.getByRole('button', { name: 'Change plan' })
     const button = screen.getByRole('button', { name: 'Change plan' })
@@ -73,11 +75,14 @@ describe('<ChangePlan />', function () {
   })
   })
 
 
   it('renders "Your new plan" and "Keep current plan" when there is a pending plan change', function () {
   it('renders "Your new plan" and "Keep current plan" when there is a pending plan change', function () {
-    window.metaAttributesCache.set('ol-subscription', pendingSubscriptionChange)
-    render(
-      <SubscriptionDashboardProvider>
-        <ActiveSubscription subscription={pendingSubscriptionChange} />
-      </SubscriptionDashboardProvider>
+    renderWithSubscriptionDashContext(
+      <ActiveSubscription subscription={pendingSubscriptionChange} />,
+      {
+        metaTags: [
+          { name: 'ol-subscription', value: pendingSubscriptionChange },
+          plansMetaTag,
+        ],
+      }
     )
     )
 
 
     const button = screen.getByRole('button', { name: 'Change plan' })
     const button = screen.getByRole('button', { name: 'Change plan' })
@@ -88,12 +93,14 @@ describe('<ChangePlan />', function () {
   })
   })
 
 
   it('does not render when Recurly did not load', function () {
   it('does not render when Recurly did not load', function () {
-    // @ts-ignore
-    delete window.recurly
-    const { container } = render(
-      <SubscriptionDashboardProvider>
-        <ActiveSubscription subscription={annualActiveSubscription} />
-      </SubscriptionDashboardProvider>
+    const { container } = renderWithSubscriptionDashContext(
+      <ActiveSubscription subscription={annualActiveSubscription} />,
+      {
+        metaTags: [
+          { name: 'ol-subscription', value: annualActiveSubscription },
+          plansMetaTag,
+        ],
+      }
     )
     )
     expect(container).not.to.be.null
     expect(container).not.to.be.null
   })
   })

+ 7 - 12
services/web/test/frontend/features/subscription/components/dashboard/subscription-dashboard.test.tsx

@@ -1,24 +1,19 @@
 import { expect } from 'chai'
 import { expect } from 'chai'
-import { render, screen } from '@testing-library/react'
+import { screen } from '@testing-library/react'
 import SubscriptionDashboard from '../../../../../../frontend/js/features/subscription/components/dashboard/subscription-dashboard'
 import SubscriptionDashboard from '../../../../../../frontend/js/features/subscription/components/dashboard/subscription-dashboard'
-import { SubscriptionDashboardProvider } from '../../../../../../frontend/js/features/subscription/context/subscription-dashboard-context'
+import {
+  cleanUpContext,
+  renderWithSubscriptionDashContext,
+} from '../../helpers/render-with-subscription-dash-context'
 
 
 describe('<SubscriptionDashboard />', function () {
 describe('<SubscriptionDashboard />', function () {
-  beforeEach(function () {
-    window.metaAttributesCache = new Map()
-  })
-
   afterEach(function () {
   afterEach(function () {
-    window.metaAttributesCache = new Map()
+    cleanUpContext()
   })
   })
 
 
   describe('Free Plan', function () {
   describe('Free Plan', function () {
     it('does not render the "Get the most out of your" subscription text', function () {
     it('does not render the "Get the most out of your" subscription text', function () {
-      render(
-        <SubscriptionDashboardProvider>
-          <SubscriptionDashboard />
-        </SubscriptionDashboardProvider>
-      )
+      renderWithSubscriptionDashContext(<SubscriptionDashboard />)
       const text = screen.queryByText('Get the most out of your', {
       const text = screen.queryByText('Get the most out of your', {
         exact: false,
         exact: false,
       })
       })

+ 38 - 0
services/web/test/frontend/features/subscription/helpers/render-with-subscription-dash-context.tsx

@@ -0,0 +1,38 @@
+import { render } from '@testing-library/react'
+import { SubscriptionDashboardProvider } from '../../../../../frontend/js/features/subscription/context/subscription-dashboard-context'
+
+export function renderWithSubscriptionDashContext(
+  component: React.ReactElement,
+  options?: {
+    metaTags?: { name: string; value: string | object | Array<object> }[]
+    recurlyNotLoaded?: boolean
+  }
+) {
+  const SubscriptionDashboardProviderWrapper = ({
+    children,
+  }: {
+    children: React.ReactNode
+  }) => (
+    <SubscriptionDashboardProvider>{children}</SubscriptionDashboardProvider>
+  )
+
+  window.metaAttributesCache = new Map()
+  options?.metaTags?.forEach(tag =>
+    window.metaAttributesCache.set(tag.name, tag.value)
+  )
+
+  if (!options?.recurlyNotLoaded) {
+    // @ts-ignore
+    window.recurly = {}
+  }
+
+  return render(component, {
+    wrapper: SubscriptionDashboardProviderWrapper,
+  })
+}
+
+export function cleanUpContext() {
+  // @ts-ignore
+  delete window.recurly
+  window.metaAttributesCache = new Map()
+}

+ 47 - 0
services/web/test/frontend/features/subscription/util/recurly-pricing.test.tsx

@@ -0,0 +1,47 @@
+import { expect } from 'chai'
+import { formatPriceForDisplayData } from '../../../../../frontend/js/features/subscription/util/recurly-pricing'
+
+describe('formatPriceForDisplayData', function () {
+  beforeEach(function () {
+    window.metaAttributesCache = new Map()
+  })
+  afterEach(function () {
+    window.metaAttributesCache = new Map()
+  })
+  it('should handle no tax rate', function () {
+    window.metaAttributesCache.set('ol-recommendedCurrency', 'USD')
+    const data = formatPriceForDisplayData('1000', 0)
+    expect(data).to.deep.equal({
+      totalForDisplay: '$1000',
+      totalAsNumber: 1000,
+      subtotal: '$1000.00',
+      tax: '$0.00',
+      includesTax: false,
+    })
+    window.metaAttributesCache = new Map()
+  })
+
+  it('should handle a tax rate', function () {
+    window.metaAttributesCache.set('ol-recommendedCurrency', 'EUR')
+    const data = formatPriceForDisplayData('380', 0.2)
+    expect(data).to.deep.equal({
+      totalForDisplay: '€456',
+      totalAsNumber: 456,
+      subtotal: '€380.00',
+      tax: '€76.00',
+      includesTax: true,
+    })
+  })
+
+  it('should handle total with cents', function () {
+    window.metaAttributesCache.set('ol-recommendedCurrency', 'EUR')
+    const data = formatPriceForDisplayData('8', 0.2)
+    expect(data).to.deep.equal({
+      totalForDisplay: '€9.60',
+      totalAsNumber: 9.6,
+      subtotal: '€8.00',
+      tax: '€1.60',
+      includesTax: true,
+    })
+  })
+})