first commit

This commit is contained in:
2025-04-24 13:11:28 +08:00
commit ff9c54d5e4
5960 changed files with 834111 additions and 0 deletions

View File

@@ -0,0 +1,24 @@
import { expect } from 'chai'
import { render, screen, within } from '@testing-library/react'
import CanceledSubscription from '../../../../../../frontend/js/features/subscription/components/canceled-subscription/canceled'
describe('canceled subscription page', function () {
it('renders the invoices link', function () {
render(<CanceledSubscription />)
screen.getByRole('heading', { name: /subscription canceled/i })
const alert = screen.getByRole('alert')
within(alert).getByText(/to modify your subscription go to/i)
const manageSubscriptionLink = within(alert).getByRole('link', {
name: /manage subscription/i,
})
expect(manageSubscriptionLink.getAttribute('href')).to.equal(
'/user/subscription'
)
const backToYourProjectsLink = screen.getByRole('link', {
name: /back to your projects/i,
})
expect(backToYourProjectsLink.getAttribute('href')).to.equal('/project')
})
})

View File

@@ -0,0 +1,17 @@
import { render, screen } from '@testing-library/react'
import FreePlan from '../../../../../../frontend/js/features/subscription/components/dashboard/free-plan'
describe('<FreePlan />', function () {
it('renders free plan dash', function () {
render(<FreePlan />)
screen.getByText(
'You are on the Overleaf Free plan. Upgrade to access these',
{
exact: false,
}
)
screen.getByText('Upgrade now')
})
})

View File

@@ -0,0 +1,154 @@
import { expect } from 'chai'
import {
fireEvent,
render,
screen,
waitFor,
within,
} from '@testing-library/react'
import sinon from 'sinon'
import GroupSubscriptionMemberships from '../../../../../../frontend/js/features/subscription/components/dashboard/group-subscription-memberships'
import { SubscriptionDashboardProvider } from '../../../../../../frontend/js/features/subscription/context/subscription-dashboard-context'
import fetchMock from 'fetch-mock'
import { MemberGroupSubscription } from '../../../../../../types/subscription/dashboard/subscription'
import {
groupActiveSubscription,
groupActiveSubscriptionWithPendingLicenseChange,
} from '../../fixtures/subscriptions'
import * as useLocationModule from '../../../../../../frontend/js/shared/hooks/use-location'
import { UserId } from '../../../../../../types/user'
import { SplitTestProvider } from '@/shared/context/split-test-context'
const userId = 'fff999fff999'
const memberGroupSubscriptions: MemberGroupSubscription[] = [
{
...groupActiveSubscription,
userIsGroupManager: false,
planLevelName: 'Professional',
admin_id: {
id: 'abc123abc123' as UserId,
email: 'you@example.com',
},
},
{
...groupActiveSubscriptionWithPendingLicenseChange,
userIsGroupManager: true,
planLevelName: 'Collaborator',
admin_id: {
id: 'bcd456bcd456' as UserId,
email: 'someone@example.com',
},
},
] as MemberGroupSubscription[]
describe('<GroupSubscriptionMemberships />', function () {
beforeEach(function () {
window.metaAttributesCache.set(
'ol-memberGroupSubscriptions',
memberGroupSubscriptions
)
window.metaAttributesCache.set('ol-user_id', userId)
})
afterEach(function () {
fetchMock.removeRoutes().clearHistory()
})
it('renders all group subscriptions not managed', function () {
render(
<SplitTestProvider>
<SubscriptionDashboardProvider>
<GroupSubscriptionMemberships />
</SubscriptionDashboardProvider>
</SplitTestProvider>
)
const elements = screen.getAllByText('You are on our', {
exact: false,
})
expect(elements.length).to.equal(1)
expect(elements[0].textContent).to.equal(
'You are on our Professional plan as a member of the group subscription GAS administered by you@example.com'
)
})
describe('opens leave group modal when button is clicked', function () {
let reloadStub: sinon.SinonStub
beforeEach(function () {
reloadStub = sinon.stub()
this.locationStub = sinon.stub(useLocationModule, 'useLocation').returns({
assign: sinon.stub(),
replace: sinon.stub(),
reload: reloadStub,
setHash: sinon.stub(),
})
render(
<SplitTestProvider>
<SubscriptionDashboardProvider>
<GroupSubscriptionMemberships />
</SubscriptionDashboardProvider>
</SplitTestProvider>
)
const leaveGroupButton = screen.getByRole('button', {
name: 'Leave group',
})
fireEvent.click(leaveGroupButton)
this.confirmModal = screen.getByRole('dialog')
within(this.confirmModal).getByText(
'Are you sure you want to leave this group?'
)
this.cancelButton = within(this.confirmModal).getByRole('button', {
name: 'Cancel',
})
this.leaveNowButton = within(this.confirmModal).getByRole('button', {
name: 'Leave now',
})
})
afterEach(function () {
this.locationStub.restore()
})
it('close the modal', function () {
fireEvent.click(this.cancelButton)
expect(screen.queryByRole('dialog')).to.not.exist
})
it('leave the group', async function () {
const leaveGroupApiMock = fetchMock.delete(
`/subscription/group/user?subscriptionId=bcd567`,
{
status: 204,
}
)
fireEvent.click(this.leaveNowButton)
expect(leaveGroupApiMock.callHistory.called()).to.be.true
await waitFor(() => {
expect(reloadStub).to.have.been.called
})
})
})
it('renders nothing when there are no group subscriptions', function () {
window.metaAttributesCache.set('ol-memberGroupSubscriptions', undefined)
render(
<SplitTestProvider>
<SubscriptionDashboardProvider>
<GroupSubscriptionMemberships />
</SubscriptionDashboardProvider>
</SplitTestProvider>
)
const elements = screen.queryAllByText('You are on our', {
exact: false,
})
expect(elements.length).to.equal(0)
})
})

View File

@@ -0,0 +1,65 @@
import { expect } from 'chai'
import { screen } from '@testing-library/react'
import InstitutionMemberships from '../../../../../../frontend/js/features/subscription/components/dashboard/institution-memberships'
import {
cleanUpContext,
renderWithSubscriptionDashContext,
} from '../../helpers/render-with-subscription-dash-context'
import { Institution } from '../../../../../../types/institution'
const memberships: Institution[] = [
{
id: 9258,
name: 'Test University',
commonsAccount: true,
isUniversity: true,
confirmed: true,
ssoBeta: false,
ssoEnabled: false,
maxConfirmationMonths: 6,
writefullCommonsAccount: false,
},
{
id: 9259,
name: 'Example Institution',
commonsAccount: true,
isUniversity: true,
confirmed: true,
ssoBeta: false,
ssoEnabled: true,
maxConfirmationMonths: 12,
writefullCommonsAccount: false,
},
]
describe('<InstitutionMemberships />', function () {
afterEach(function () {
cleanUpContext()
})
it('renders all insitutions with license', function () {
renderWithSubscriptionDashContext(<InstitutionMemberships />, {
metaTags: [
{ name: 'ol-currentInstitutionsWithLicence', value: memberships },
],
})
const elements = screen.getAllByText('You are on our', {
exact: false,
})
expect(elements.length).to.equal(2)
expect(elements[0].textContent).to.equal(
'You are on our Professional plan as a confirmed member of Test University'
)
expect(elements[1].textContent).to.equal(
'You are on our Professional plan as a confirmed member of Example Institution'
)
})
it('renders error message when failed to check commons licenses', function () {
renderWithSubscriptionDashContext(<InstitutionMemberships />)
screen.getByText(
'Sorry, something went wrong. Subscription information related to institutional affiliations may not be displayed. Please try again later.'
)
})
})

View File

@@ -0,0 +1,236 @@
import { expect } from 'chai'
import { screen } from '@testing-library/react'
import ManagedGroupSubscriptions from '../../../../../../frontend/js/features/subscription/components/dashboard/managed-group-subscriptions'
import { ManagedGroupSubscription } from '../../../../../../types/subscription/dashboard/subscription'
import {
cleanUpContext,
renderWithSubscriptionDashContext,
} from '../../helpers/render-with-subscription-dash-context'
function getManagedGroupSubscriptions(
groupSSO: boolean | null,
managedUsers: boolean | null
): ManagedGroupSubscription[] {
const subscriptionOne = {
_id: 'bcd567',
userIsGroupMember: true,
planLevelName: 'Professional',
admin_id: {
email: 'you@example.com',
},
features: {
groupSSO,
managedUsers,
},
teamName: 'GAS',
}
const subscriptionTwo = {
_id: 'def456',
userIsGroupMember: false,
planLevelName: 'Collaborator',
admin_id: {
email: 'someone@example.com',
},
features: {
groupSSO,
managedUsers,
},
teamName: 'GASWPLC',
}
const subscriptionMemberAndAdmin = {
_id: 'group2abc',
userIsGroupMember: true,
planLevelName: 'Collaborator',
admin_id: {
email: 'admin@example.com',
},
features: {
groupSSO,
managedUsers,
},
teamName: 'Testing',
}
const subscriptionAdmin = {
_id: 'group123abc',
userIsGroupMember: false,
planLevelName: 'Collaborator',
admin_id: {
email: 'admin@example.com',
},
features: {
groupSSO,
managedUsers,
},
teamName: 'Testing Another',
}
return [
subscriptionOne,
subscriptionTwo,
subscriptionMemberAndAdmin,
subscriptionAdmin,
]
}
const managedGroupSubscriptions: ManagedGroupSubscription[] =
getManagedGroupSubscriptions(false, false)
const managedGroupSubscriptions2: ManagedGroupSubscription[] =
getManagedGroupSubscriptions(true, true)
const managedGroupSubscriptions3: ManagedGroupSubscription[] =
getManagedGroupSubscriptions(true, false)
const managedGroupSubscriptions4: ManagedGroupSubscription[] =
getManagedGroupSubscriptions(false, true)
describe('<ManagedGroupSubscriptions />', function () {
afterEach(function () {
cleanUpContext()
})
it('renders all managed group subscriptions', async function () {
renderWithSubscriptionDashContext(<ManagedGroupSubscriptions />, {
metaTags: [
{
name: 'ol-managedGroupSubscriptions',
value: managedGroupSubscriptions,
},
{ name: 'ol-usersEmail', value: 'admin@example.com' },
],
})
const elements = screen.getAllByText('You are a', {
exact: false,
})
expect(elements.length).to.equal(4)
expect(elements[0].textContent).to.equal(
'You are a manager and member of the Professional group subscription GAS administered by you@example.com.'
)
expect(elements[1].textContent).to.equal(
'You are a manager of the Collaborator group subscription GASWPLC administered by someone@example.com.'
)
expect(elements[2].textContent).to.equal(
'You are a manager and member of the Collaborator group subscription Testing administered by you (admin@example.com).'
)
expect(elements[3].textContent).to.equal(
'You are a manager of the Collaborator group subscription Testing Another administered by you (admin@example.com).'
)
const links = screen.getAllByRole('link')
expect(links[1].getAttribute('href')).to.equal(
'/manage/groups/bcd567/members'
)
expect(links[2].getAttribute('href')).to.equal(
'/manage/groups/bcd567/managers'
)
expect(links[3].getAttribute('href')).to.equal('/metrics/groups/bcd567')
expect(links[5].getAttribute('href')).to.equal(
'/manage/groups/def456/members'
)
expect(links[6].getAttribute('href')).to.equal(
'/manage/groups/def456/managers'
)
expect(links[7].getAttribute('href')).to.equal('/metrics/groups/def456')
expect(links[9].getAttribute('href')).to.equal(
'/manage/groups/group2abc/members'
)
expect(links[10].getAttribute('href')).to.equal(
'/manage/groups/group2abc/managers'
)
expect(links[11].getAttribute('href')).to.equal('/metrics/groups/group2abc')
expect(links[13].getAttribute('href')).to.equal(
'/manage/groups/group123abc/members'
)
expect(links[14].getAttribute('href')).to.equal(
'/manage/groups/group123abc/managers'
)
expect(links[15].getAttribute('href')).to.equal(
'/metrics/groups/group123abc'
)
})
it('renders nothing when there are no group memberships', function () {
renderWithSubscriptionDashContext(<ManagedGroupSubscriptions />)
const elements = screen.queryAllByText('You are a', {
exact: false,
})
expect(elements.length).to.equal(0)
})
it('does not render the Manage group settings row when the user is not the group admin', function () {
renderWithSubscriptionDashContext(<ManagedGroupSubscriptions />, {
metaTags: [
{
name: 'ol-managedGroupSubscriptions',
value: managedGroupSubscriptions2,
},
{
name: 'ol-groupSettingsEnabledFor',
value: [],
},
],
})
expect(screen.queryByText('Manage group settings')).to.be.null
expect(screen.queryByText('Configure and manage SSO and Managed Users')).to
.be.null
})
it('renders Managed Group / Group SSO settings row when both features are turned on', async function () {
renderWithSubscriptionDashContext(<ManagedGroupSubscriptions />, {
metaTags: [
{
name: 'ol-managedGroupSubscriptions',
value: managedGroupSubscriptions2,
},
{
name: 'ol-groupSettingsEnabledFor',
value: [managedGroupSubscriptions2[0]._id],
},
],
})
await screen.findAllByText('Group settings')
await screen.findAllByText('Configure and manage SSO and Managed Users')
})
it('does not render Group SSO settings when the feature is turned off', async function () {
renderWithSubscriptionDashContext(<ManagedGroupSubscriptions />, {
metaTags: [
{
name: 'ol-managedGroupSubscriptions',
value: managedGroupSubscriptions4,
},
{
name: 'ol-groupSettingsEnabledFor',
value: [managedGroupSubscriptions4[0]._id],
},
],
})
await screen.findAllByText('Group settings')
await screen.findAllByText('Turn on Managed Users')
expect(screen.queryByText('Configure and manage SSO and Managed Users')).to
.not.exist
expect(screen.queryByText('Configure and manage SSO')).to.not.exist
})
it('does not render MAnaged Group settings when the feature is turned off', async function () {
renderWithSubscriptionDashContext(<ManagedGroupSubscriptions />, {
metaTags: [
{
name: 'ol-managedGroupSubscriptions',
value: managedGroupSubscriptions3,
},
{
name: 'ol-groupSettingsEnabledFor',
value: [managedGroupSubscriptions3[0]._id],
},
],
})
await screen.findAllByText('Group settings')
await screen.findAllByText('Configure and manage SSO')
expect(screen.queryByText('Turn on Managed Users')).to.not.exist
expect(screen.queryByText('Configure and manage SSO and Managed Users')).to
.not.exist
})
})

View File

@@ -0,0 +1,152 @@
import { expect } from 'chai'
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
import ManagedInstitutions from '../../../../../../frontend/js/features/subscription/components/dashboard/managed-institutions'
import { SubscriptionDashboardProvider } from '../../../../../../frontend/js/features/subscription/context/subscription-dashboard-context'
import fetchMock from 'fetch-mock'
import { SplitTestProvider } from '@/shared/context/split-test-context'
import { ManagedInstitution } from '../../../../../../types/subscription/dashboard/managed-institution'
const userId = 'fff999fff999'
const institution1 = {
v1Id: 123,
managerIds: [],
metricsEmail: {
optedOutUserIds: [],
lastSent: new Date(),
},
name: 'Inst 1',
}
const institution2 = {
v1Id: 456,
managerIds: [],
metricsEmail: {
optedOutUserIds: [userId],
lastSent: new Date(),
},
name: 'Inst 2',
}
const managedInstitutions: ManagedInstitution[] = [institution1, institution2]
describe('<ManagedInstitutions />', function () {
beforeEach(function () {
window.metaAttributesCache.set(
'ol-managedInstitutions',
managedInstitutions
)
window.metaAttributesCache.set('ol-user_id', userId)
})
afterEach(function () {
fetchMock.removeRoutes().clearHistory()
})
it('renders all managed institutions', function () {
render(
<SplitTestProvider>
<SubscriptionDashboardProvider>
<ManagedInstitutions />
</SubscriptionDashboardProvider>
</SplitTestProvider>
)
const elements = screen.getAllByText('You are a', {
exact: false,
})
expect(elements.length).to.equal(2)
expect(elements[0].textContent).to.equal(
'You are a manager of the Overleaf Commons subscription at Inst 1'
)
expect(elements[1].textContent).to.equal(
'You are a manager of the Overleaf Commons subscription at Inst 2'
)
const links = screen.getAllByRole('link')
expect(links[0].getAttribute('href')).to.equal('/metrics/institutions/123')
expect(links[1].getAttribute('href')).to.equal('/institutions/123/hub')
expect(links[2].getAttribute('href')).to.equal(
'/manage/institutions/123/managers'
)
expect(links[3].getAttribute('href')).to.equal('/metrics/institutions/456')
expect(links[4].getAttribute('href')).to.equal('/institutions/456/hub')
expect(links[5].getAttribute('href')).to.equal(
'/manage/institutions/456/managers'
)
const subscribeLinks = screen.getAllByText('Subscribe')
expect(subscribeLinks.length).to.equal(1)
const unsubscribeLinks = screen.getAllByText('Unsubscribe')
expect(unsubscribeLinks.length).to.equal(1)
})
it('clicking unsubscribe should unsubscribe from metrics emails', async function () {
window.metaAttributesCache.set('ol-managedInstitutions', [institution1])
const unsubscribeUrl = '/institutions/123/emailSubscription'
fetchMock.post(unsubscribeUrl, {
status: 204,
body: [userId],
})
render(
<SplitTestProvider>
<SubscriptionDashboardProvider>
<ManagedInstitutions />
</SubscriptionDashboardProvider>
</SplitTestProvider>
)
const unsubscribeLink = screen.getByText('Unsubscribe')
await fireEvent.click(unsubscribeLink)
await waitFor(
() => expect(fetchMock.callHistory.called(unsubscribeUrl)).to.be.true
)
await waitFor(() => {
expect(screen.getByText('Subscribe')).to.exist
})
})
it('clicking subscribe should subscribe to metrics emails', async function () {
window.metaAttributesCache.set('ol-managedInstitutions', [institution2])
const subscribeUrl = '/institutions/456/emailSubscription'
fetchMock.post(subscribeUrl, {
status: 204,
body: [],
})
render(
<SplitTestProvider>
<SubscriptionDashboardProvider>
<ManagedInstitutions />
</SubscriptionDashboardProvider>
</SplitTestProvider>
)
const subscribeLink = await screen.findByText('Subscribe')
await fireEvent.click(subscribeLink)
await waitFor(
() => expect(fetchMock.callHistory.called(subscribeUrl)).to.be.true
)
await screen.findByText('Unsubscribe')
})
it('renders nothing when there are no institutions', function () {
window.metaAttributesCache.set('ol-managedInstitutions', undefined)
render(
<SplitTestProvider>
<SubscriptionDashboardProvider>
<ManagedInstitutions />
</SubscriptionDashboardProvider>
</SplitTestProvider>
)
const elements = screen.queryAllByText('You are a', {
exact: false,
})
expect(elements.length).to.equal(0)
})
})

View File

@@ -0,0 +1,76 @@
import { expect } from 'chai'
import { render, screen } from '@testing-library/react'
import { SubscriptionDashboardProvider } from '../../../../../../frontend/js/features/subscription/context/subscription-dashboard-context'
import fetchMock from 'fetch-mock'
import ManagedPublishers from '../../../../../../frontend/js/features/subscription/components/dashboard/managed-publishers'
import { SplitTestProvider } from '@/shared/context/split-test-context'
import { Publisher } from '../../../../../../types/subscription/dashboard/publisher'
const userId = 'fff999fff999'
const publisher1 = {
slug: 'pub-1',
managerIds: [],
name: 'Pub 1',
partner: 'p1',
}
const publisher2 = {
slug: 'pub-2',
managerIds: [],
name: 'Pub 2',
partner: 'p2',
}
const managedPublishers: Publisher[] = [publisher1, publisher2]
describe('<ManagedPublishers />', function () {
beforeEach(function () {
window.metaAttributesCache.set('ol-managedPublishers', managedPublishers)
window.metaAttributesCache.set('ol-user_id', userId)
})
afterEach(function () {
fetchMock.removeRoutes().clearHistory()
})
it('renders all managed publishers', function () {
render(
<SplitTestProvider>
<SubscriptionDashboardProvider>
<ManagedPublishers />
</SubscriptionDashboardProvider>
</SplitTestProvider>
)
const elements = screen.getAllByText('You are a', {
exact: false,
})
expect(elements.length).to.equal(2)
expect(elements[0].textContent).to.equal('You are a manager of Pub 1')
expect(elements[1].textContent).to.equal('You are a manager of Pub 2')
const links = screen.getAllByRole('link')
expect(links[0].getAttribute('href')).to.equal('/publishers/pub-1/hub')
expect(links[1].getAttribute('href')).to.equal(
'/manage/publishers/pub-1/managers'
)
expect(links[2].getAttribute('href')).to.equal('/publishers/pub-2/hub')
expect(links[3].getAttribute('href')).to.equal(
'/manage/publishers/pub-2/managers'
)
})
it('renders nothing when there are no publishers', function () {
window.metaAttributesCache.set('ol-managedPublishers', undefined)
render(
<SplitTestProvider>
<SubscriptionDashboardProvider>
<ManagedPublishers />
</SubscriptionDashboardProvider>
</SplitTestProvider>
)
const elements = screen.queryAllByText('You are a', {
exact: false,
})
expect(elements.length).to.equal(0)
})
})

View File

@@ -0,0 +1,142 @@
import { fireEvent, screen, waitFor } from '@testing-library/react'
import fetchMock from 'fetch-mock'
import sinon from 'sinon'
import { expect } from 'chai'
import {
annualActiveSubscription,
groupActiveSubscription,
monthlyActiveCollaborator,
trialSubscription,
} from '../../fixtures/subscriptions'
import { renderActiveSubscription } from '../../helpers/render-active-subscription'
import * as useLocationModule from '../../../../../../frontend/js/shared/hooks/use-location'
import { MetaTag } from '@/utils/meta'
const pauseSubscriptionSplitTestMeta: MetaTag[] = [
{ name: 'ol-splitTestVariants', value: { 'pause-subscription': 'enabled' } },
]
function renderSubscriptionWithPauseSupport(
subscription = monthlyActiveCollaborator
) {
return renderActiveSubscription(subscription, pauseSubscriptionSplitTestMeta)
}
function clickCancelButton() {
const button = screen.getByRole('button', {
name: /Cancel your subscription/i,
})
fireEvent.click(button)
}
function clickDurationSelect() {
const pauseDurationSelect = screen.getByLabelText('Pause subscription for', {
selector: 'input',
})
fireEvent.click(pauseDurationSelect)
}
function clickSubmitButton() {
const buttonConfirm = screen.getByRole('button', {
name: 'Pause subscription',
})
fireEvent.click(buttonConfirm)
}
describe('<PauseSubscriptionModal />', function () {
beforeEach(function () {
reloadStub = sinon.stub()
this.locationStub = sinon.stub(useLocationModule, 'useLocation').returns({
assign: sinon.stub(),
replace: sinon.stub(),
reload: reloadStub,
setHash: sinon.stub(),
toString: sinon
.stub()
.returns('https://www.dev-overleaf.com/user/subscription'),
})
this.replaceStateStub = sinon.stub(window.history, 'replaceState')
})
afterEach(function () {
fetchMock.removeRoutes().clearHistory()
this.locationStub.restore()
this.replaceStateStub.restore()
})
it('does not render with an annual subscription', async function () {
renderSubscriptionWithPauseSupport(annualActiveSubscription)
clickCancelButton()
// goes straight to cancel
await screen.findByText('Wed love you to stay')
})
it('does not render with a group plan', async function () {
renderSubscriptionWithPauseSupport(groupActiveSubscription)
clickCancelButton()
// goes straight to cancel
await screen.findByText('Wed love you to stay')
})
it('does not render when in a trial', async function () {
renderSubscriptionWithPauseSupport(trialSubscription)
clickCancelButton()
await screen.findByText('Wed love you to stay')
})
it('renders when trying to cancel subscription', async function () {
renderSubscriptionWithPauseSupport()
clickCancelButton()
await screen.findByText('Pause instead, to pick up where you left off')
})
let reloadStub: sinon.SinonStub
it('renders options for pause duration', async function () {
renderSubscriptionWithPauseSupport()
clickCancelButton()
clickDurationSelect()
await screen.findByRole('option', { name: '1 month' })
await screen.findByRole('option', { name: '2 months' })
await screen.findByRole('option', { name: '3 months' })
})
it('changes to selected duration', async function () {
renderSubscriptionWithPauseSupport()
clickCancelButton()
clickDurationSelect()
const twoMonthsOption = await screen.findByRole('option', {
name: '2 months',
selected: false,
})
fireEvent.click(twoMonthsOption)
clickDurationSelect()
await screen.findByRole('option', { name: '2 months', selected: true })
})
it('shows error if pausing failed', async function () {
const endPointResponse = {
status: 500,
}
fetchMock.post(`/user/subscription/pause/1`, endPointResponse)
renderSubscriptionWithPauseSupport()
clickCancelButton()
clickSubmitButton()
await screen.findByText('Sorry, something went wrong. ', {
exact: false,
})
})
it('reloads if pause successful', async function () {
const endPointResponse = {
status: 200,
}
fetchMock.post(`/user/subscription/pause/1`, endPointResponse)
renderSubscriptionWithPauseSupport()
clickCancelButton()
clickSubmitButton()
await waitFor(() => {
expect(reloadStub).to.have.been.called
})
})
})

View File

@@ -0,0 +1,237 @@
import { expect } from 'chai'
import {
screen,
fireEvent,
waitForElementToBeRemoved,
within,
} from '@testing-library/react'
import PersonalSubscription from '../../../../../../frontend/js/features/subscription/components/dashboard/personal-subscription'
import {
annualActiveSubscription,
canceledSubscription,
customSubscription,
pastDueExpiredSubscription,
} from '../../fixtures/subscriptions'
import {
cleanUpContext,
renderWithSubscriptionDashContext,
} from '../../helpers/render-with-subscription-dash-context'
import { reactivateSubscriptionUrl } from '../../../../../../frontend/js/features/subscription/data/subscription-url'
import fetchMock from 'fetch-mock'
import sinon from 'sinon'
import * as useLocationModule from '../../../../../../frontend/js/shared/hooks/use-location'
describe('<PersonalSubscription />', function () {
afterEach(function () {
cleanUpContext()
})
describe('no subscription', function () {
it('returns empty container', function () {
const { container } = renderWithSubscriptionDashContext(
<PersonalSubscription />
)
expect(container.firstChild).to.be.null
})
})
describe('custom subscription', function () {
it('displays contact support message', function () {
renderWithSubscriptionDashContext(<PersonalSubscription />, {
metaTags: [{ name: 'ol-subscription', value: customSubscription }],
})
screen.getByText('Please', { exact: false })
screen.getByText('contact support', { exact: false })
screen.getByText('to make changes to your plan', { exact: false })
})
})
describe('subscription states ', function () {
let reloadStub: sinon.SinonStub
beforeEach(function () {
reloadStub = sinon.stub()
this.locationStub = sinon.stub(useLocationModule, 'useLocation').returns({
assign: sinon.stub(),
replace: sinon.stub(),
reload: reloadStub,
setHash: sinon.stub(),
toString: sinon.stub(),
})
})
afterEach(function () {
this.locationStub.restore()
})
it('renders the active dash', function () {
renderWithSubscriptionDashContext(<PersonalSubscription />, {
metaTags: [
{ name: 'ol-subscription', value: annualActiveSubscription },
],
})
screen.getByRole('heading', { name: /billing/i })
})
it('renders the canceled dash', function () {
renderWithSubscriptionDashContext(<PersonalSubscription />, {
metaTags: [{ name: 'ol-subscription', value: canceledSubscription }],
})
screen.getByText(
'Your subscription has been canceled and will terminate on',
{ exact: false }
)
screen.getByText(canceledSubscription.payment!.nextPaymentDueAt, {
exact: false,
})
screen.getByText('No further payments will be taken.', { exact: false })
screen.getByRole('button', { name: 'View your invoices' })
screen.getByRole('button', { name: 'Reactivate your subscription' })
})
it('reactivates canceled plan', async function () {
renderWithSubscriptionDashContext(<PersonalSubscription />, {
metaTags: [{ name: 'ol-subscription', value: canceledSubscription }],
})
const reactivateBtn = screen.getByRole<HTMLButtonElement>('button', {
name: 'Reactivate your subscription',
})
// 1st click - fail
fetchMock.postOnce(reactivateSubscriptionUrl, 400)
fireEvent.click(reactivateBtn)
expect(reactivateBtn.disabled).to.be.true
await fetchMock.callHistory.flush(true)
expect(reactivateBtn.disabled).to.be.false
expect(reloadStub).not.to.have.been.called
fetchMock.removeRoutes().clearHistory()
// 2nd click - success
fetchMock.postOnce(reactivateSubscriptionUrl, 200)
fireEvent.click(reactivateBtn)
await fetchMock.callHistory.flush(true)
expect(reloadStub).to.have.been.calledOnce
expect(reactivateBtn.disabled).to.be.true
fetchMock.removeRoutes().clearHistory()
})
it('renders the expired dash', function () {
renderWithSubscriptionDashContext(<PersonalSubscription />, {
metaTags: [
{ name: 'ol-subscription', value: pastDueExpiredSubscription },
],
})
screen.getByText('Your subscription has expired.')
})
it('renders error message when an unknown subscription state', function () {
const withStateDeleted = Object.assign(
{},
JSON.parse(JSON.stringify(annualActiveSubscription))
)
withStateDeleted.payment.state = undefined
renderWithSubscriptionDashContext(<PersonalSubscription />, {
metaTags: [{ name: 'ol-subscription', value: withStateDeleted }],
})
screen.getByText(
'There is a problem with your subscription. Please contact us for more information.'
)
})
})
describe('past due subscription', function () {
it('renders error alert', function () {
renderWithSubscriptionDashContext(<PersonalSubscription />, {
metaTags: [
{ name: 'ol-subscription', value: pastDueExpiredSubscription },
],
})
screen.getByRole('alert')
screen.getByText(
'Your account currently has a past due invoice. You will not be able to change your plan until this is resolved.',
{ exact: false }
)
const invoiceLinks = screen.getAllByText('View Your Invoices', {
exact: false,
})
expect(invoiceLinks.length).to.equal(2)
})
})
describe('Recurly JS', function () {
const recurlyFailedToLoadText =
'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 () {
renderWithSubscriptionDashContext(<PersonalSubscription />, {
metaTags: [
{ name: 'ol-subscription', value: annualActiveSubscription },
],
recurlyNotLoaded: true,
})
screen.getByRole('alert')
screen.getByText(recurlyFailedToLoadText)
expect(screen.queryByText('Change plan')).to.be.null
})
it('should not show an alert and should show "Change plan" option when Recurly did load', function () {
renderWithSubscriptionDashContext(<PersonalSubscription />, {
metaTags: [
{ name: 'ol-subscription', value: annualActiveSubscription },
],
})
expect(screen.queryByRole('alert')).to.be.null
screen.getByText('Change plan')
})
})
it('shows different payment email address section', async function () {
fetchMock.post('/user/subscription/account/email', 200)
const usersEmail = 'foo@example.com'
renderWithSubscriptionDashContext(<PersonalSubscription />, {
metaTags: [
{ name: 'ol-subscription', value: annualActiveSubscription },
{ name: 'ol-usersEmail', value: usersEmail },
],
})
const billingText = screen.getByText(
/your billing email address is currently/i
).textContent
expect(billingText).to.contain(
`Your billing email address is currently ${annualActiveSubscription.payment.accountEmail}.` +
` If needed you can update your billing address to ${usersEmail}`
)
const submitBtn = screen.getByRole<HTMLButtonElement>('button', {
name: /update/i,
})
expect(submitBtn.disabled).to.be.false
fireEvent.click(submitBtn)
expect(submitBtn.disabled).to.be.true
expect(
screen.getByRole<HTMLButtonElement>('button', { name: /updating/i })
.disabled
).to.be.true
await waitForElementToBeRemoved(() =>
screen.getByText(/your billing email address is currently/i)
)
within(screen.getByRole('alert')).getByText(
/your billing email address was successfully updated/i
)
expect(screen.queryByRole('button', { name: /update/i })).to.be.null
expect(screen.queryByRole('button', { name: /updating/i })).to.be.null
})
})

View File

@@ -0,0 +1,533 @@
import { expect } from 'chai'
import { fireEvent, screen, waitFor } from '@testing-library/react'
import * as eventTracking from '@/infrastructure/event-tracking'
import { PaidSubscription } from '../../../../../../../../types/subscription/dashboard/subscription'
import {
annualActiveSubscription,
groupActiveSubscription,
groupActiveSubscriptionWithPendingLicenseChange,
monthlyActiveCollaborator,
pendingSubscriptionChange,
trialCollaboratorSubscription,
trialSubscription,
} from '../../../../fixtures/subscriptions'
import sinon from 'sinon'
import { cleanUpContext } from '../../../../helpers/render-with-subscription-dash-context'
import { renderActiveSubscription } from '../../../../helpers/render-active-subscription'
import { cloneDeep } from 'lodash'
import fetchMock from 'fetch-mock'
import {
cancelSubscriptionUrl,
extendTrialUrl,
subscriptionUpdateUrl,
} from '@/features/subscription/data/subscription-url'
import * as useLocationModule from '../../../../../../../../frontend/js/shared/hooks/use-location'
import { MetaTag } from '@/utils/meta'
describe('<ActiveSubscription />', function () {
let sendMBSpy: sinon.SinonSpy
beforeEach(function () {
sendMBSpy = sinon.spy(eventTracking, 'sendMB')
})
afterEach(function () {
cleanUpContext()
sendMBSpy.restore()
})
function expectedInActiveSubscription(subscription: PaidSubscription) {
// sentence broken up by bolding
screen.getByText('You are currently subscribed to the', { exact: false })
screen.getByText(subscription.plan.name, { exact: false })
screen.getByRole('button', { name: 'Change plan' })
// sentence broken up by bolding
screen.getByText('The next payment of', { exact: false })
screen.getByText(subscription.payment.displayPrice, {
exact: false,
})
screen.getByText('will be collected on', { exact: false })
const dates = screen.getAllByText(subscription.payment.nextPaymentDueAt, {
exact: false,
})
expect(dates.length).to.equal(2)
screen.getByText(
'* Prices may be subject to additional VAT, depending on your country.'
)
screen.getByRole('link', { name: 'Update your billing details' })
screen.getByRole('link', { name: 'View your invoices' })
}
it('renders the dash annual active subscription', function () {
renderActiveSubscription(annualActiveSubscription)
expectedInActiveSubscription(annualActiveSubscription)
})
it('shows change plan UI when button clicked', async function () {
renderActiveSubscription(annualActiveSubscription)
const button = screen.getByRole('button', { name: 'Change plan' })
fireEvent.click(button)
// confirm main dash UI still shown
screen.getByText('You are currently subscribed to the', { exact: false })
await screen.findByRole('heading', { name: 'Change plan' })
expect(
screen.getAllByRole('button', { name: 'Change to this plan' }).length > 0
).to.be.true
})
it('notes when user is changing plan at end of current plan term', function () {
renderActiveSubscription(pendingSubscriptionChange)
expectedInActiveSubscription(pendingSubscriptionChange)
screen.getByText('Your plan is changing to', { exact: false })
screen.getByText(pendingSubscriptionChange.pendingPlan!.name)
screen.getByText(' at the end of the current billing period', {
exact: false,
})
screen.getByText(
'If you wish this change to apply before the end of your current billing period, please contact us.'
)
expect(screen.queryByRole('link', { name: 'contact support' })).to.be.null
expect(screen.queryByText('if you wish to change your group subscription.'))
.to.be.null
})
it('does not show "Change plan" option when past due', function () {
// account is likely in expired state, but be sure to not show option if state is still active
const activePastDueSubscription = Object.assign(
{},
JSON.parse(JSON.stringify(annualActiveSubscription))
)
activePastDueSubscription.payment.hasPastDueInvoice = true
renderActiveSubscription(activePastDueSubscription)
const changePlan = screen.queryByRole('button', { name: 'Change plan' })
expect(changePlan).to.be.null
})
it('shows the pending license change message when plan change is pending', function () {
renderActiveSubscription(groupActiveSubscriptionWithPendingLicenseChange)
screen.getByText('Your subscription is changing to include', {
exact: false,
})
screen.getByText(
groupActiveSubscriptionWithPendingLicenseChange.payment
.pendingAdditionalLicenses!
)
screen.getByText('additional license(s) for a total of', { exact: false })
screen.getByText(
groupActiveSubscriptionWithPendingLicenseChange.payment
.pendingTotalLicenses!
)
expect(
screen.queryByText(
'If you wish this change to apply before the end of your current billing period, please contact us.'
)
).to.be.null
})
it('shows the pending license change message when plan change is not pending', function () {
const subscription = Object.assign({}, groupActiveSubscription)
subscription.payment.additionalLicenses = 4
subscription.payment.totalLicenses =
subscription.payment.totalLicenses +
subscription.payment.additionalLicenses
renderActiveSubscription(subscription)
screen.getByText('Your subscription includes', {
exact: false,
})
screen.getByText(subscription.payment.additionalLicenses)
screen.getByText('additional license(s) for a total of', { exact: false })
screen.getByText(subscription.payment.totalLicenses)
})
it('shows when trial ends and first payment collected and when subscription would become inactive if cancelled', function () {
renderActiveSubscription(trialSubscription)
screen.getByText('Youre on a free trial which ends on', { exact: false })
const endDate = screen.getAllByText(
trialSubscription.payment.trialEndsAtFormatted!
)
expect(endDate.length).to.equal(3)
})
it('shows current discounts', function () {
const subscriptionWithActiveCoupons = cloneDeep(annualActiveSubscription)
subscriptionWithActiveCoupons.payment.activeCoupons = [
{
name: 'fake coupon name',
code: 'fake-coupon',
description: '',
},
]
renderActiveSubscription(subscriptionWithActiveCoupons)
screen.getByText(
/this does not include your current discounts, which will be applied automatically before your next payment/i
)
screen.getByText(
subscriptionWithActiveCoupons.payment.activeCoupons[0].name
)
})
describe('cancel plan', function () {
const assignStub = sinon.stub()
const reloadStub = sinon.stub()
beforeEach(function () {
this.locationStub = sinon.stub(useLocationModule, 'useLocation').returns({
assign: assignStub,
replace: sinon.stub(),
reload: reloadStub,
setHash: sinon.stub(),
toString: sinon.stub(),
})
})
afterEach(function () {
this.locationStub.restore()
fetchMock.removeRoutes().clearHistory()
})
function showConfirmCancelUI() {
const button = screen.getByRole('button', {
name: 'Cancel your subscription',
})
fireEvent.click(button)
}
it('shows cancel UI', function () {
renderActiveSubscription(annualActiveSubscription)
screen.getByText(
'Your subscription will remain active until the end of your billing period',
{ exact: false }
)
const dates = screen.getAllByText(
annualActiveSubscription.payment.nextPaymentDueAt,
{
exact: false,
}
)
expect(dates.length).to.equal(2)
const button = screen.getByRole('button', {
name: 'Cancel your subscription',
})
expect(button).to.exist
})
it('shows cancel UI when still in a trial period', function () {
renderActiveSubscription(trialSubscription)
screen.getByText(
'Your subscription will remain active until the end of your trial period',
{ exact: false }
)
const dates = screen.getAllByText(
trialSubscription.payment.trialEndsAtFormatted!
)
expect(dates.length).to.equal(3)
const button = screen.getByRole('button', {
name: 'Cancel your subscription',
})
expect(button).to.exist
})
it('shows cancel prompt on button click and sends event', function () {
renderActiveSubscription(annualActiveSubscription)
showConfirmCancelUI()
expect(sendMBSpy).to.be.calledOnceWith(
'subscription-page-cancel-button-click'
)
screen.getByText('Wed love you to stay')
screen.getByRole('button', { name: 'Cancel my subscription' })
})
it('cancels subscription and redirects page', async function () {
const endPointResponse = {
status: 200,
}
fetchMock.post(cancelSubscriptionUrl, endPointResponse)
renderActiveSubscription(annualActiveSubscription)
showConfirmCancelUI()
const button = screen.getByRole('button', {
name: 'Cancel my subscription',
})
fireEvent.click(button)
await waitFor(() => {
expect(assignStub).to.have.been.called
})
sinon.assert.calledWithMatch(assignStub, '/user/subscription/canceled')
})
it('shows an error message if canceling subscription failed', async function () {
const endPointResponse = {
status: 500,
}
fetchMock.post(cancelSubscriptionUrl, endPointResponse)
renderActiveSubscription(annualActiveSubscription)
showConfirmCancelUI()
const button = screen.getByRole('button', {
name: 'Cancel my subscription',
})
fireEvent.click(button)
await screen.findByText('Sorry, something went wrong. ', {
exact: false,
})
screen.getByText('Please try again. ', { exact: false })
screen.getByText('If the problem continues please contact us.', {
exact: false,
})
})
it('disables cancels subscription button after clicking and shows loading spinner', async function () {
renderActiveSubscription(annualActiveSubscription)
showConfirmCancelUI()
screen.getByRole('button', {
name: 'I want to stay',
})
const button = screen.getByRole('button', {
name: 'Cancel my subscription',
})
fireEvent.click(button)
const cancelButton = screen.getByRole('button', {
name: 'Processing…',
}) as HTMLButtonElement
expect(cancelButton.disabled).to.be.true
const hiddenText = screen.getByText('Cancel my subscription')
expect(hiddenText.getAttribute('aria-hidden')).to.equal('true')
})
describe('extend trial', function () {
const canExtend: MetaTag = {
name: 'ol-userCanExtendTrial',
value: true,
}
const cancelButtonText = 'No thanks, I still want to cancel'
const extendTrialButtonText = 'Ill take it!'
it('shows alternate cancel subscription button text for cancel button and option to extend trial', function () {
renderActiveSubscription(trialCollaboratorSubscription, [canExtend])
showConfirmCancelUI()
screen.getByText('Have another', { exact: false })
screen.getByText('14 days', { exact: false })
screen.getByText('on your Trial!', { exact: false })
screen.getByRole('button', {
name: cancelButtonText,
})
screen.getByRole('button', {
name: extendTrialButtonText,
})
})
it('disables both buttons and updates text for when trial button clicked', function () {
renderActiveSubscription(trialCollaboratorSubscription, [canExtend])
showConfirmCancelUI()
const extendTrialButton = screen.getByRole('button', {
name: extendTrialButtonText,
})
fireEvent.click(extendTrialButton)
const buttons = screen.getAllByRole('button')
expect(buttons.length).to.equal(2)
expect(buttons[0].getAttribute('disabled')).to.equal('')
expect(buttons[1].getAttribute('disabled')).to.equal('')
screen.getByRole('button', {
name: cancelButtonText,
})
screen.getByRole('button', {
name: 'Processing…',
})
})
it('disables both buttons and updates text for when cancel button clicked', function () {
renderActiveSubscription(trialCollaboratorSubscription, [canExtend])
showConfirmCancelUI()
const cancelButtton = screen.getByRole('button', {
name: cancelButtonText,
})
fireEvent.click(cancelButtton)
const buttons = screen.getAllByRole('button')
expect(buttons.length).to.equal(2)
expect(buttons[0].getAttribute('disabled')).to.equal('')
expect(buttons[1].getAttribute('disabled')).to.equal('')
screen.getByRole('button', {
name: 'Processing…',
})
screen.getByRole('button', {
name: extendTrialButtonText,
})
})
it('does not show option to extend trial when user is not eligible', function () {
renderActiveSubscription(trialCollaboratorSubscription)
showConfirmCancelUI()
expect(
screen.queryByRole('button', {
name: extendTrialButtonText,
})
).to.be.null
})
it('reloads page after the successful request to extend trial', async function () {
const endPointResponse = {
status: 200,
}
fetchMock.put(extendTrialUrl, endPointResponse)
renderActiveSubscription(trialCollaboratorSubscription, [canExtend])
showConfirmCancelUI()
const extendTrialButton = screen.getByRole('button', {
name: extendTrialButtonText,
})
fireEvent.click(extendTrialButton)
// page is reloaded on success
await waitFor(() => {
expect(reloadStub).to.have.been.called
})
})
})
describe('downgrade plan', function () {
const cancelButtonText = 'No thanks, I still want to cancel'
const downgradeButtonText = 'Yes, move me to the Personal plan'
it('shows alternate cancel subscription button text', async function () {
renderActiveSubscription(monthlyActiveCollaborator)
showConfirmCancelUI()
await screen.findByRole('button', {
name: cancelButtonText,
})
screen.getByRole('button', {
name: downgradeButtonText,
})
screen.getByText('Would you be interested in the cheaper', {
exact: false,
})
screen.getByText('Personal plan?', {
exact: false,
})
})
it('disables both buttons and updates text for when trial button clicked', async function () {
renderActiveSubscription(monthlyActiveCollaborator)
showConfirmCancelUI()
const downgradeButton = await screen.findByRole('button', {
name: downgradeButtonText,
})
fireEvent.click(downgradeButton)
const buttons = screen.getAllByRole('button')
expect(buttons.length).to.equal(2)
expect(buttons[0].getAttribute('disabled')).to.equal('')
expect(buttons[1].getAttribute('disabled')).to.equal('')
screen.getByRole('button', {
name: cancelButtonText,
})
screen.getByRole('button', {
name: 'Processing…',
})
})
it('disables both buttons and updates text for when cancel button clicked', async function () {
renderActiveSubscription(monthlyActiveCollaborator)
showConfirmCancelUI()
const cancelButtton = await screen.findByRole('button', {
name: cancelButtonText,
})
fireEvent.click(cancelButtton)
const buttons = screen.getAllByRole('button')
expect(buttons.length).to.equal(2)
expect(buttons[0].getAttribute('disabled')).to.equal('')
expect(buttons[1].getAttribute('disabled')).to.equal('')
screen.getByRole('button', {
name: 'Processing…',
})
screen.getByRole('button', {
name: downgradeButtonText,
})
})
it('does not show option to downgrade when not a collaborator plan', function () {
const trialPlan = cloneDeep(monthlyActiveCollaborator)
trialPlan.plan.planCode = 'anotherplan'
renderActiveSubscription(trialPlan)
showConfirmCancelUI()
expect(
screen.queryByRole('button', {
name: downgradeButtonText,
})
).to.be.null
})
it('does not show option to extend trial when on a collaborator trial', function () {
const trialPlan = cloneDeep(trialCollaboratorSubscription)
renderActiveSubscription(trialPlan)
showConfirmCancelUI()
expect(
screen.queryByRole('button', {
name: downgradeButtonText,
})
).to.be.null
})
it('reloads page after the successful request to downgrade plan', async function () {
const endPointResponse = {
status: 200,
}
fetchMock.post(subscriptionUpdateUrl, endPointResponse)
renderActiveSubscription(monthlyActiveCollaborator)
showConfirmCancelUI()
const downgradeButton = await screen.findByRole('button', {
name: downgradeButtonText,
})
fireEvent.click(downgradeButton)
// page is reloaded on success
await waitFor(() => {
expect(reloadStub).to.have.been.called
})
})
})
})
describe('group plans', function () {
it('does not show "Change plan" option for group plans', function () {
renderActiveSubscription(groupActiveSubscription)
const changePlan = screen.queryByRole('button', { name: 'Change plan' })
expect(changePlan).to.be.null
})
it('shows contact support message for group plan change requests', function () {
renderActiveSubscription(groupActiveSubscription)
screen.getByRole('link', { name: 'contact support' })
screen.getByText('if you wish to change your group subscription.', {
exact: false,
})
})
})
})

View File

@@ -0,0 +1,561 @@
import { expect } from 'chai'
import { fireEvent, screen, waitFor, within } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { groupPlans, plans } from '../../../../../fixtures/plans'
import {
annualActiveSubscription,
annualActiveSubscriptionEuro,
annualActiveSubscriptionPro,
pendingSubscriptionChange,
} from '../../../../../fixtures/subscriptions'
import { ActiveSubscription } from '../../../../../../../../../frontend/js/features/subscription/components/dashboard/states/active/active'
import {
cleanUpContext,
renderWithSubscriptionDashContext,
} from '../../../../../helpers/render-with-subscription-dash-context'
import sinon from 'sinon'
import fetchMock from 'fetch-mock'
import {
cancelPendingSubscriptionChangeUrl,
subscriptionUpdateUrl,
} from '../../../../../../../../../frontend/js/features/subscription/data/subscription-url'
import { renderActiveSubscription } from '../../../../../helpers/render-active-subscription'
import * as useLocationModule from '../../../../../../../../../frontend/js/shared/hooks/use-location'
describe('<ChangePlanModal />', function () {
let reloadStub: sinon.SinonStub
beforeEach(function () {
reloadStub = sinon.stub()
this.locationStub = sinon.stub(useLocationModule, 'useLocation').returns({
assign: sinon.stub(),
replace: sinon.stub(),
reload: reloadStub,
setHash: sinon.stub(),
toString: sinon.stub(),
})
})
afterEach(function () {
cleanUpContext()
fetchMock.removeRoutes().clearHistory()
this.locationStub.restore()
})
it('renders the individual plans table and group plans UI', async function () {
renderActiveSubscription(annualActiveSubscription)
const button = screen.getByRole('button', { name: 'Change plan' })
fireEvent.click(button)
await screen.findByText('Looking for multiple licenses?')
const changeToPlanButtons = screen.queryAllByRole('button', {
name: 'Change to this plan',
})
expect(changeToPlanButtons.length).to.equal(plans.length - 3) // excludes paid-personal and paid-personal-annual
screen.getByText('Your plan')
const annualPlans = plans.filter(plan => plan.annual)
expect(screen.getAllByText('/ year', { exact: false }).length).to.equal(
annualPlans.length - 1
) // excludes paid-personal-annual
expect(screen.getAllByText('/ month', { exact: false }).length).to.equal(
plans.length - annualPlans.length - 1
) // excludes paid-personal
expect(screen.queryByText('loading', { exact: false })).to.be.null
})
it('renders "Your new plan" and "Keep current plan" when there is a pending plan change', async function () {
renderActiveSubscription(pendingSubscriptionChange)
const button = screen.getByRole('button', { name: 'Change plan' })
fireEvent.click(button)
await screen.findByText('Your new plan')
screen.getByRole('button', { name: 'Keep my current plan' })
})
it('does not render when Recurly did not load', function () {
const { container } = renderWithSubscriptionDashContext(
<ActiveSubscription subscription={annualActiveSubscription} />,
{
metaTags: [
{ name: 'ol-subscription', value: annualActiveSubscription },
{ name: 'ol-plans', value: plans },
],
}
)
expect(container).not.to.be.null
})
it('shows a loading message while still querying Recurly for prices', async function () {
renderWithSubscriptionDashContext(
<ActiveSubscription subscription={pendingSubscriptionChange} />,
{
metaTags: [
{ name: 'ol-subscription', value: pendingSubscriptionChange },
],
}
)
const button = screen.getByRole('button', { name: 'Change plan' })
fireEvent.click(button)
await screen.findByText('Loading', { exact: false })
})
describe('Change plan modal', function () {
it('open confirmation modal when "Change to this plan" clicked', async function () {
renderActiveSubscription(annualActiveSubscription)
const button = screen.getByRole('button', { name: 'Change plan' })
fireEvent.click(button)
const buttons = await screen.findAllByRole('button', {
name: 'Change to this plan',
})
fireEvent.click(buttons[0])
const confirmModal = screen.getByRole('dialog')
await within(confirmModal).findByText(
'Are you sure you want to change plan to',
{
exact: false,
}
)
within(confirmModal).getByRole('button', { name: 'Change plan' })
expect(
screen.queryByText(
'Your existing plan and its features will remain active until the end of the current billing period.'
)
).to.be.null
expect(
screen.queryByText(
'If you wish this change to apply before the end of your current billing period, please contact us.'
)
).to.be.null
})
it('shows message in confirmation dialog about plan remaining active until end of term when expected', async function () {
let planIndex = 0
const planThatWillChange = plans.find((p, i) => {
if (p.planCode !== annualActiveSubscription.planCode) {
planIndex = i
}
return p.planCode !== annualActiveSubscription.planCode
})
renderActiveSubscription(annualActiveSubscription, [
{
name: 'ol-planCodesChangingAtTermEnd',
value: [planThatWillChange!.planCode],
},
])
const button = screen.getByRole('button', { name: 'Change plan' })
fireEvent.click(button)
const buttons = await screen.findAllByRole('button', {
name: 'Change to this plan',
})
fireEvent.click(buttons[planIndex])
const confirmModal = screen.getByRole('dialog')
await within(confirmModal).findByText(
'Your existing plan and its features will remain active until the end of the current billing period.'
)
screen.getByText(
'If you wish this change to apply before the end of your current billing period, please contact us.'
)
})
it('changes plan after confirmed in modal', async function () {
const endPointResponse = {
status: 200,
}
fetchMock.post(
`${subscriptionUpdateUrl}?origin=confirmChangePlan`,
endPointResponse
)
renderActiveSubscription(annualActiveSubscription, [
{
name: 'ol-planCodesChangingAtTermEnd',
value: [annualActiveSubscription.planCode],
},
])
const button = screen.getByRole('button', { name: 'Change plan' })
fireEvent.click(button)
const buttons = await screen.findAllByRole('button', {
name: 'Change to this plan',
})
fireEvent.click(buttons[0])
await screen.findByText('Are you sure you want to change plan to', {
exact: false,
})
const buttonConfirm = within(screen.getByRole('dialog')).getByRole(
'button',
{ name: 'Change plan' }
)
fireEvent.click(buttonConfirm)
screen.getByRole('button', { name: 'Processing…' })
// page is reloaded on success
await waitFor(() => {
expect(reloadStub).to.have.been.called
})
})
it('shows error if changing plan failed', async function () {
const endPointResponse = {
status: 500,
}
fetchMock.post(
`${subscriptionUpdateUrl}?origin=confirmChangePlan`,
endPointResponse
)
renderActiveSubscription(annualActiveSubscription)
const button = screen.getByRole('button', { name: 'Change plan' })
fireEvent.click(button)
const buttons = await screen.findAllByRole('button', {
name: 'Change to this plan',
})
fireEvent.click(buttons[0])
await screen.findByText('Are you sure you want to change plan to', {
exact: false,
})
const buttonConfirm = within(screen.getByRole('dialog')).getByRole(
'button',
{ name: 'Change plan' }
)
fireEvent.click(buttonConfirm)
screen.getByRole('button', { name: 'Processing…' })
await screen.findByText('Sorry, something went wrong. ', { exact: false })
await screen.findByText('Please try again. ', { exact: false })
await screen.findByText('If the problem continues please contact us.', {
exact: false,
})
expect(
within(screen.getByRole('dialog'))
.getByRole('button', { name: 'Change plan' })
.getAttribute('disabled')
).to.not.exist
})
})
describe('Keep current plan modal', function () {
let confirmModal: HTMLElement
beforeEach(async function () {
renderActiveSubscription(pendingSubscriptionChange)
const button = screen.getByRole('button', { name: 'Change plan' })
fireEvent.click(button)
const keepPlanButton = await screen.findByRole('button', {
name: 'Keep my current plan',
})
fireEvent.click(keepPlanButton)
confirmModal = screen.getByRole('dialog')
})
it('opens confirmation modal when "Keep my current plan" is clicked', async function () {
within(confirmModal).getByText(
'Are you sure you want to revert your scheduled plan change? You will remain subscribed to the',
{
exact: false,
}
)
screen.getByRole('button', { name: 'Revert scheduled plan change' })
})
it('keeps current plan when "Revert scheduled plan change" is clicked in modal', async function () {
const endPointResponse = {
status: 200,
}
fetchMock.post(cancelPendingSubscriptionChangeUrl, endPointResponse)
const buttonConfirm = within(confirmModal).getByRole('button', {
name: 'Revert scheduled plan change',
})
fireEvent.click(buttonConfirm)
screen.getByRole('button', { name: 'Processing…' })
// page is reloaded on success
await waitFor(() => {
expect(reloadStub).to.have.been.called
})
})
it('shows error if keeping plan failed', async function () {
const endPointResponse = {
status: 500,
}
fetchMock.post(cancelPendingSubscriptionChangeUrl, endPointResponse)
const buttonConfirm = within(confirmModal).getByRole('button', {
name: 'Revert scheduled plan change',
})
fireEvent.click(buttonConfirm)
screen.getByRole('button', { name: 'Processing…' })
await screen.findByText('Sorry, something went wrong. ', { exact: false })
await screen.findByText('Please try again. ', { exact: false })
await screen.findByText('If the problem continues please contact us.', {
exact: false,
})
expect(
within(screen.getByRole('dialog'))
.getByRole('button', { name: 'Revert scheduled plan change' })
.getAttribute('disabled')
).to.not.exist
})
})
describe('Change to group plan modal', function () {
const standardPlanCollaboratorText = '10 collaborators per project'
const professionalPlanCollaboratorText = 'Unlimited collaborators'
const educationInputLabel =
'Get a total of 40% off for groups using Overleaf for teaching'
let modal: HTMLElement
async function openModal() {
const button = screen.getByRole('button', { name: 'Change plan' })
fireEvent.click(button)
const buttonGroupModal = await screen.findByRole('button', {
name: 'Change to a group plan',
})
fireEvent.click(buttonGroupModal)
modal = await screen.findByRole('dialog')
}
it('open group plan modal "Change to a group plan" clicked', async function () {
renderActiveSubscription(annualActiveSubscription)
await openModal()
within(modal).getByText('Customize your group subscription')
within(modal).getByText('$1,290 per year')
expect(within(modal).getAllByText('$129 per user').length).to.equal(2)
within(modal).getByText('Each user will have access to:')
within(modal).getByText('All premium features')
within(modal).getByText('Sync with Dropbox and GitHub')
within(modal).getByText('Full document history')
within(modal).getByText('plus more')
within(modal).getByText(standardPlanCollaboratorText)
expect(within(modal).queryByText(professionalPlanCollaboratorText)).to.be
.null
const plans = within(modal).getByRole('group')
const planOptions = within(plans).getAllByRole('radio')
expect(planOptions.length).to.equal(groupPlans.plans.length)
const standardPlanRadioInput = within(modal).getByLabelText(
'Standard'
) as HTMLInputElement
expect(standardPlanRadioInput.checked).to.be.true
const sizeSelect = within(modal).getByRole('combobox') as HTMLInputElement
expect(sizeSelect.value).to.equal('10')
const sizeOption = within(sizeSelect).getAllByRole('option')
expect(sizeOption.length).to.equal(groupPlans.sizes.length)
within(modal).getByText(
'Get a total of 40% off for groups using Overleaf for teaching'
)
const educationalCheckbox = within(modal).getByRole(
'checkbox'
) as HTMLInputElement
expect(educationalCheckbox.checked).to.be.false
within(modal).getByText(
'Your new subscription will be billed immediately to your current payment method.'
)
expect(within(modal).queryByText('tax', { exact: false })).to.be.null
within(modal).getByRole('button', { name: 'Upgrade now' })
within(modal).getByRole('button', {
name: 'Need more than 20 licenses? Please get in touch',
})
})
it('changes the collaborator count when the plan changes', async function () {
renderActiveSubscription(annualActiveSubscription)
await openModal()
const professionalPlanOption =
within(modal).getByLabelText('Professional')
fireEvent.click(professionalPlanOption)
await within(modal).findByText(professionalPlanCollaboratorText)
expect(within(modal).queryByText(standardPlanCollaboratorText)).to.be.null
})
it('shows educational discount applied when input checked', async function () {
const discountAppliedText = '40% educational discount applied!'
renderActiveSubscription(annualActiveSubscription)
await openModal()
const educationInput = within(modal).getByLabelText(educationInputLabel)
fireEvent.click(educationInput)
await within(modal).findByText(discountAppliedText)
const sizeSelect = within(modal).getByRole('combobox') as HTMLInputElement
await userEvent.selectOptions(sizeSelect, [screen.getByText('5')])
await within(modal).findByText(discountAppliedText)
})
it('shows total with tax when tax applied', async function () {
renderActiveSubscription(annualActiveSubscriptionEuro, undefined, 'EUR')
await openModal()
within(modal).getByText('Total:', { exact: false })
expect(
within(modal).getAllByText('€1,438.40', { exact: false }).length
).to.equal(3)
within(modal).getByText('(€1,160.00 + €278.40 tax) per year', {
exact: false,
})
})
it('changes the price when options change', async function () {
renderActiveSubscription(annualActiveSubscription)
await openModal()
within(modal).getByText('$1,290 per year')
within(modal).getAllByText('$129 per user')
// plan type (pro collab)
let standardPlanRadioInput = within(modal).getByLabelText(
'Standard'
) as HTMLInputElement
expect(standardPlanRadioInput.checked).to.be.true
let professionalPlanRadioInput = within(modal).getByLabelText(
'Professional'
) as HTMLInputElement
expect(professionalPlanRadioInput.checked).to.be.false
fireEvent.click(professionalPlanRadioInput)
standardPlanRadioInput = within(modal).getByLabelText(
'Standard'
) as HTMLInputElement
expect(standardPlanRadioInput.checked).to.be.false
professionalPlanRadioInput = within(modal).getByLabelText(
'Professional'
) as HTMLInputElement
expect(professionalPlanRadioInput.checked).to.be.true
await within(modal).findByText('$2,590 per year')
await within(modal).findAllByText('$259 per user')
// user count
let sizeSelect = within(modal).getByRole('combobox') as HTMLInputElement
expect(sizeSelect.value).to.equal('10')
await userEvent.selectOptions(sizeSelect, [screen.getByText('5')])
sizeSelect = within(modal).getByRole('combobox') as HTMLInputElement
expect(sizeSelect.value).to.equal('5')
await within(modal).findByText('$1,395 per year')
await within(modal).findAllByText('$279 per user')
// usage (enterprise or educational)
let educationInput = within(modal).getByLabelText(
educationInputLabel
) as HTMLInputElement
expect(educationInput.checked).to.be.false
fireEvent.click(educationInput)
educationInput = within(modal).getByLabelText(
educationInputLabel
) as HTMLInputElement
expect(educationInput.checked).to.be.true
// make sure doesn't change price until back at min user to qualify
await within(modal).findByText('$1,395 per year')
await within(modal).findAllByText('$279 per user')
await userEvent.selectOptions(sizeSelect, [screen.getByText('10')])
await within(modal).findByText('$1,550 per year')
await within(modal).findAllByText('$155 per user')
})
it('has pro as the default group plan type if user is on a pro plan', async function () {
renderActiveSubscription(annualActiveSubscriptionPro)
await openModal()
const standardPlanRadioInput = within(modal).getByLabelText(
'Professional'
) as HTMLInputElement
expect(standardPlanRadioInput.checked).to.be.true
})
it('submits the changes and reloads the page', async function () {
const endPointResponse = {
status: 200,
}
fetchMock.post(subscriptionUpdateUrl, endPointResponse)
renderActiveSubscription(annualActiveSubscriptionPro)
await openModal()
const buttonConfirm = screen.getByRole('button', { name: 'Upgrade now' })
fireEvent.click(buttonConfirm)
screen.getByRole('button', { name: 'Processing…' })
// // page is reloaded on success
await waitFor(() => {
expect(reloadStub).to.have.been.called
})
})
it('shows message if error after submitting form', async function () {
const endPointResponse = {
status: 500,
}
fetchMock.post(subscriptionUpdateUrl, endPointResponse)
renderActiveSubscription(annualActiveSubscriptionPro)
await openModal()
const buttonConfirm = screen.getByRole('button', { name: 'Upgrade now' })
fireEvent.click(buttonConfirm)
screen.getByRole('button', { name: 'Processing…' })
await screen.findByText('Sorry, something went wrong. ', { exact: false })
await screen.findByText('Please try again. ', { exact: false })
await screen.findByText('If the problem continues please contact us.', {
exact: false,
})
})
})
})

View File

@@ -0,0 +1,13 @@
import { render, screen } from '@testing-library/react'
import { ExpiredSubscription } from '../../../../../../../frontend/js/features/subscription/components/dashboard/states/expired'
import { pastDueExpiredSubscription } from '../../../fixtures/subscriptions'
describe('<ExpiredSubscription />', function () {
it('renders the invoices link', function () {
render(<ExpiredSubscription subscription={pastDueExpiredSubscription} />)
screen.getByText('View Your Invoices', {
exact: false,
})
})
})

View File

@@ -0,0 +1,98 @@
import { expect } from 'chai'
import { screen } from '@testing-library/react'
import SubscriptionDashboard from '../../../../../../frontend/js/features/subscription/components/dashboard/subscription-dashboard'
import {
cleanUpContext,
renderWithSubscriptionDashContext,
} from '../../helpers/render-with-subscription-dash-context'
import { groupPlans, plans } from '../../fixtures/plans'
import { annualActiveSubscription } from '../../fixtures/subscriptions'
describe('<SubscriptionDashboard />', function () {
afterEach(function () {
cleanUpContext()
})
describe('With an active subscription', function () {
beforeEach(function () {
renderWithSubscriptionDashContext(<SubscriptionDashboard />, {
metaTags: [
{ name: 'ol-plans', value: plans },
{
name: 'ol-groupPlans',
value: groupPlans,
},
{ name: 'ol-subscription', value: annualActiveSubscription },
{
name: 'ol-recommendedCurrency',
value: 'USD',
},
],
})
})
it('renders the "Get the most from your subscription" text', function () {
screen.getByText(/get the most from your Overleaf subscription/i)
})
})
describe('Free Plan', function () {
beforeEach(function () {
renderWithSubscriptionDashContext(<SubscriptionDashboard />)
})
it('does not render the "Get the most out of your" subscription text', function () {
const text = screen.queryByText('Get the most out of your', {
exact: false,
})
expect(text).to.be.null
})
it('does not render the contact support message', function () {
const text = screen.queryByText(
`Youre on an Overleaf Paid plan. Contact`,
{
exact: false,
}
)
expect(text).to.be.null
})
})
describe('Custom subscription', function () {
it('renders the contact support message', function () {
renderWithSubscriptionDashContext(<SubscriptionDashboard />, {
metaTags: [
{
name: 'ol-currentInstitutionsWithLicence',
value: [],
},
{
name: 'ol-hasSubscription',
value: true,
},
],
})
screen.getByText(`Youre on an Overleaf Paid plan.`, {
exact: false,
})
screen.getByText(`Contact support`, {
exact: false,
})
})
})
it('Show a warning when coming from plans page', function () {
renderWithSubscriptionDashContext(<SubscriptionDashboard />, {
metaTags: [
{
name: 'ol-fromPlansPage',
value: true,
},
],
})
screen.getByText('You already have a subscription')
})
})

View File

@@ -0,0 +1,35 @@
import { render, screen } from '@testing-library/react'
import AcceptedInvite from '../../../../../../frontend/js/features/subscription/components/group-invite/accepted-invite'
import { expect } from 'chai'
describe('accepted group invite', function () {
it('renders', async function () {
window.metaAttributesCache.set('ol-inviterName', 'example@overleaf.com')
render(<AcceptedInvite />)
await screen.findByText(
'You have joined the group subscription managed by example@overleaf.com'
)
})
it('links to SSO enrollment page for SSO groups', async function () {
window.metaAttributesCache.set('ol-inviterName', 'example@overleaf.com')
window.metaAttributesCache.set('ol-groupSSOActive', true)
window.metaAttributesCache.set('ol-subscriptionId', 'group123')
render(<AcceptedInvite />)
const linkBtn = (await screen.findByRole('link', {
name: 'Done',
})) as HTMLLinkElement
expect(linkBtn.href).to.equal(
'https://www.test-overleaf.com/subscription/group123/sso_enrollment'
)
})
it('links to dash for non-SSO groups', async function () {
window.metaAttributesCache.set('ol-inviterName', 'example@overleaf.com')
render(<AcceptedInvite />)
const linkBtn = (await screen.findByRole('link', {
name: 'Done',
})) as HTMLLinkElement
expect(linkBtn.href).to.equal('https://www.test-overleaf.com/project')
})
})

View File

@@ -0,0 +1,120 @@
import { render, screen } from '@testing-library/react'
import { expect } from 'chai'
import GroupInvite from '../../../../../../frontend/js/features/subscription/components/group-invite/group-invite'
describe('group invite', function () {
const inviterName = 'example@overleaf.com'
beforeEach(function () {
window.metaAttributesCache.set('ol-inviterName', inviterName)
})
it('renders header', async function () {
render(<GroupInvite />)
await screen.findByText(inviterName)
screen.getByText(`has invited you to join a group subscription on Overleaf`)
expect(screen.queryByText('Email link expired, please request a new one.'))
.to.be.null
})
describe('when user has personal subscription', function () {
beforeEach(function () {
window.metaAttributesCache.set(
'ol-hasIndividualRecurlySubscription',
true
)
})
it('renders cancel personal subscription view', async function () {
render(<GroupInvite />)
await screen.findByText(
'You already have an individual subscription, would you like us to cancel this first before joining the group licence?'
)
})
describe('and in a managed group', function () {
// note: this should not be possible but managed user view takes priority over all
beforeEach(function () {
window.metaAttributesCache.set(
'ol-currentManagedUserAdminEmail',
'example@overleaf.com'
)
window.metaAttributesCache.set('ol-cannot-join-subscription', true)
})
it('renders managed user cannot join view', async function () {
render(<GroupInvite />)
await screen.findByText('You cant join this group subscription')
screen.getByText(
'Your Overleaf account is managed by your current group admin (example@overleaf.com). This means you cant join additional group subscriptions',
{ exact: false }
)
screen.getByRole('link', { name: 'Read more about Managed Users.' })
})
})
})
describe('when user does not have a personal subscription', function () {
beforeEach(function () {
window.metaAttributesCache.set(
'ol-hasIndividualRecurlySubscription',
false
)
window.metaAttributesCache.set('ol-inviteToken', 'token123')
})
it('does not render cancel personal subscription view', async function () {
render(<GroupInvite />)
await screen.findByText(
'Please click the button below to join the group subscription and enjoy the benefits of an upgraded Overleaf account'
)
})
})
describe('when the user is already a managed user in another group', function () {
beforeEach(function () {
window.metaAttributesCache.set(
'ol-currentManagedUserAdminEmail',
'example@overleaf.com'
)
window.metaAttributesCache.set('ol-cannot-join-subscription', true)
})
it('renders managed user cannot join view', async function () {
render(<GroupInvite />)
await screen.findByText(inviterName)
screen.getByText(
`has invited you to join a group subscription on Overleaf`
)
screen.getByText('You cant join this group subscription')
screen.getByText(
'Your Overleaf account is managed by your current group admin (example@overleaf.com). This means you cant join additional group subscriptions',
{ exact: false }
)
screen.getByRole('link', { name: 'Read more about Managed Users.' })
})
})
describe('expired', function () {
beforeEach(function () {
window.metaAttributesCache.set('ol-expired', true)
})
it('shows error notification when expired', async function () {
render(<GroupInvite />)
await screen.findByText('Email link expired, please request a new one.')
})
})
describe('join view', function () {
beforeEach(function () {
window.metaAttributesCache.set('ol-inviteToken', 'token123')
})
it('shows view to join group', async function () {
render(<GroupInvite />)
await screen.findByText(
'Please click the button below to join the group subscription and enjoy the benefits of an upgraded Overleaf account'
)
})
})
})

View File

@@ -0,0 +1,48 @@
import { expect } from 'chai'
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
import HasIndividualRecurlySubscription from '../../../../../../frontend/js/features/subscription/components/group-invite/has-individual-recurly-subscription'
import sinon from 'sinon'
import fetchMock from 'fetch-mock'
describe('group invite', function () {
describe('user has a personal subscription', function () {
afterEach(function () {
fetchMock.removeRoutes().clearHistory()
})
it('shows option to cancel subscription', async function () {
render(<HasIndividualRecurlySubscription setView={() => {}} />)
await screen.findByText(
'You already have an individual subscription, would you like us to cancel this first before joining the group licence?'
)
screen.getByRole('button', { name: 'Not now' })
screen.getByRole('button', { name: 'Cancel your subscription' })
})
it('handles subscription cancellation and calls to change invite view', async function () {
fetchMock.post('/user/subscription/cancel', 200)
const setView = sinon.stub()
render(<HasIndividualRecurlySubscription setView={setView} />)
const button = await screen.findByRole('button', {
name: 'Cancel your subscription',
})
fireEvent.click(button)
await waitFor(() => {
expect(setView).to.have.been.calledOnce
})
})
it('shows error message when cancelling subscription fails', async function () {
render(<HasIndividualRecurlySubscription setView={() => {}} />)
const button = await screen.findByRole('button', {
name: 'Cancel your subscription',
})
fireEvent.click(button)
await waitFor(() => {
screen.getByText(
'Something went wrong canceling your subscription. Please contact support.'
)
})
})
})
})

View File

@@ -0,0 +1,48 @@
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
import { expect } from 'chai'
import JoinGroup from '../../../../../../frontend/js/features/subscription/components/group-invite/join-group'
import fetchMock from 'fetch-mock'
import sinon from 'sinon'
describe('join group', function () {
const inviteToken = 'token123'
beforeEach(function () {
window.metaAttributesCache.set('ol-inviteToken', inviteToken)
})
afterEach(function () {
fetchMock.removeRoutes().clearHistory()
})
it('shows option to join subscription', async function () {
render(<JoinGroup setView={() => {}} />)
await screen.findByText(
'Please click the button below to join the group subscription and enjoy the benefits of an upgraded Overleaf account'
)
screen.getByRole('link', { name: 'Not now' })
screen.getByRole('button', { name: 'Accept invitation' })
})
it('handles success when accepting invite', async function () {
fetchMock.put(`/subscription/invites/${inviteToken}`, 200)
const setView = sinon.stub()
render(<JoinGroup setView={setView} />)
const button = await screen.getByRole('button', {
name: 'Accept invitation',
})
fireEvent.click(button)
await waitFor(() => {
expect(setView).to.have.been.calledOnce
})
})
it('handles errors when accepting invite', async function () {
render(<JoinGroup setView={() => {}} />)
const button = await screen.getByRole('button', {
name: 'Accept invitation',
})
fireEvent.click(button)
await waitFor(() => {
screen.getByText('Sorry, something went wrong')
})
})
})

View File

@@ -0,0 +1,21 @@
import { render, screen } from '@testing-library/react'
import ManagedUserCannotJoin from '../../../../../../frontend/js/features/subscription/components/group-invite/managed-user-cannot-join'
describe('ManagedUserCannotJoin', function () {
beforeEach(function () {
window.metaAttributesCache.set(
'ol-currentManagedUserAdminEmail',
'example@overleaf.com'
)
window.metaAttributesCache.set('ol-cannot-join-subscription', true)
})
it('renders the component', async function () {
render(<ManagedUserCannotJoin />)
await screen.findByText(
'Your Overleaf account is managed by your current group admin (example@overleaf.com). This means you cant join additional group subscriptions',
{ exact: false }
)
screen.getByRole('link', { name: 'Read more about Managed Users.' })
})
})

View File

@@ -0,0 +1,80 @@
import { expect } from 'chai'
import { screen, within } from '@testing-library/react'
import SuccessfulSubscription from '../../../../../../frontend/js/features/subscription/components/successful-subscription/successful-subscription'
import { renderWithSubscriptionDashContext } from '../../helpers/render-with-subscription-dash-context'
import { annualActiveSubscription } from '../../fixtures/subscriptions'
import { ExposedSettings } from '../../../../../../types/exposed-settings'
describe('successful subscription page', function () {
it('renders the invoices link', function () {
const adminEmail = 'foo@example.com'
renderWithSubscriptionDashContext(<SuccessfulSubscription />, {
metaTags: [
{
name: 'ol-ExposedSettings',
value: {
adminEmail,
} as ExposedSettings,
},
{ name: 'ol-subscription', value: annualActiveSubscription },
],
})
screen.getByRole('heading', { name: /thanks for subscribing/i })
const alert = screen.getByRole('alert')
within(alert).getByText(/to modify your subscription go to/i)
const manageSubscriptionLink = within(alert).getByRole('link', {
name: /manage subscription/i,
})
expect(manageSubscriptionLink.getAttribute('href')).to.equal(
'/user/subscription'
)
screen.getByText(
`Thank you for subscribing to the ${annualActiveSubscription.plan.name} plan.`,
{ exact: false }
)
screen.getByText(
/its support from people like yourself that allows .* to continue to grow and improve/i
)
expect(screen.getByText(/get the most from your/i).textContent).to.match(
/get the most from your .* subscription\. discover premium features/i
)
expect(
screen
.getByText(/if there is anything you ever/i)
.textContent?.replace(/\xA0/g, ' ')
).to.equal(
`If there is anything you ever need please feel free to contact us directly at ${adminEmail}.`
)
const contactLink = screen.getByRole('link', {
name: adminEmail,
})
expect(contactLink.getAttribute('href')).to.equal(`mailto:${adminEmail}`)
expect(
screen.getByText(/if you would like to help us improve/i).textContent
).to.match(
/if you would like to help us improve .*, please take a moment to fill out this survey/i
)
const surveyLink = screen.getByRole('link', {
name: /this survey/i,
})
expect(surveyLink.getAttribute('href')).to.equal(
'https://forms.gle/CdLNX9m6NLxkv1yr5'
)
const helpLink = screen.getByRole('link', {
name: /discover premium features/i,
})
expect(helpLink.getAttribute('href')).to.equal(
'/learn/how-to/Overleaf_premium_features'
)
const backToYourProjectsLink = screen.getByRole('link', {
name: /back to your projects/i,
})
expect(backToYourProjectsLink.getAttribute('href')).to.equal('/project')
})
})

View File

@@ -0,0 +1,405 @@
import { GroupPlans } from '../../../../../types/subscription/dashboard/group-plans'
import { Plan } from '../../../../../types/subscription/plan'
const features = {
student: {
collaborators: 6,
dropbox: true,
versioning: true,
github: true,
templates: true,
references: true,
referencesSearch: true,
gitBridge: true,
zotero: true,
mendeley: true,
compileTimeout: 240,
compileGroup: 'priority',
trackChanges: true,
symbolPalette: true,
},
personal: {
collaborators: 1,
dropbox: true,
versioning: true,
github: true,
gitBridge: true,
templates: true,
references: true,
referencesSearch: true,
zotero: true,
mendeley: true,
compileTimeout: 240,
compileGroup: 'priority',
trackChanges: false,
symbolPalette: true,
},
collaborator: {
collaborators: 10,
dropbox: true,
versioning: true,
github: true,
templates: true,
references: true,
referencesSearch: true,
zotero: true,
gitBridge: true,
mendeley: true,
compileTimeout: 240,
compileGroup: 'priority',
trackChanges: true,
symbolPalette: true,
},
professional: {
collaborators: -1,
dropbox: true,
versioning: true,
github: true,
templates: true,
references: true,
referencesSearch: true,
zotero: true,
gitBridge: true,
mendeley: true,
compileTimeout: 240,
compileGroup: 'priority',
trackChanges: true,
symbolPalette: true,
},
}
const studentAccounts: Array<Plan> = [
{
planCode: 'student',
name: 'Student',
price_in_cents: 1000,
features: features.student,
featureDescription: [],
},
{
planCode: 'student-annual',
name: 'Student Annual',
price_in_cents: 9900,
annual: true,
features: features.student,
featureDescription: [],
},
{
planCode: 'student_free_trial',
name: 'Student',
price_in_cents: 800,
features: features.student,
hideFromUsers: true,
featureDescription: [],
},
{
planCode: 'student_free_trial_7_days',
name: 'Student',
price_in_cents: 1000,
features: features.student,
hideFromUsers: true,
featureDescription: [],
},
]
const individualMonthlyPlans: Array<Plan> = [
{
planCode: 'paid-personal',
name: 'Personal',
price_in_cents: 1500,
features: features.personal,
featureDescription: [],
},
{
planCode: 'paid-personal_free_trial_7_days',
name: 'Personal (Hidden)',
price_in_cents: 1500,
features: features.personal,
featureDescription: [],
hideFromUsers: true,
},
{
planCode: 'collaborator',
name: 'Standard (Collaborator)',
price_in_cents: 2300,
features: features.collaborator,
featureDescription: [],
},
{
planCode: 'professional',
name: 'Professional',
price_in_cents: 4500,
features: features.professional,
featureDescription: [],
},
{
planCode: 'collaborator_free_trial',
name: 'Standard (Collaborator) (Hidden)',
price_in_cents: 1900,
features: features.collaborator,
hideFromUsers: true,
featureDescription: [],
},
{
planCode: 'collaborator_free_trial_14_days',
name: 'Standard (Collaborator) (Hidden)',
price_in_cents: 1900,
features: features.collaborator,
hideFromUsers: true,
featureDescription: [],
},
{
planCode: 'collaborator_free_trial_7_days',
name: 'Standard (Collaborator) (Hidden)',
price_in_cents: 2300,
features: features.collaborator,
hideFromUsers: true,
featureDescription: [],
},
{
planCode: 'collaborator-annual_free_trial',
name: 'Standard (Collaborator) Annual (Hidden)',
price_in_cents: 18000,
features: features.collaborator,
hideFromUsers: true,
featureDescription: [],
},
{
planCode: 'professional_free_trial',
name: 'Professional (Hidden)',
price_in_cents: 3000,
features: features.professional,
hideFromUsers: true,
featureDescription: [],
},
{
planCode: 'professional_free_trial_7_days',
name: 'Professional (Hidden)',
price_in_cents: 4500,
features: features.professional,
hideFromUsers: true,
featureDescription: [],
},
]
const individualAnnualPlans: Array<Plan> = [
{
planCode: 'paid-personal-annual',
name: 'Personal Annual',
price_in_cents: 13900,
annual: true,
features: features.personal,
featureDescription: [],
},
{
planCode: 'collaborator-annual',
name: 'Standard (Collaborator) Annual',
price_in_cents: 21900,
annual: true,
features: features.collaborator,
featureDescription: [],
},
{
planCode: 'professional-annual',
name: 'Professional Annual',
price_in_cents: 42900,
annual: true,
features: features.professional,
featureDescription: [],
},
]
export const plans = [
...studentAccounts,
...individualMonthlyPlans,
...individualAnnualPlans,
]
export const groupPlans: GroupPlans = {
plans: [
{
display: 'Standard',
code: 'collaborator',
},
{
display: 'Professional',
code: 'professional',
},
],
sizes: ['2', '3', '4', '5', '10', '20'],
}
export const groupPriceByUsageTypeAndSize = {
educational: {
professional: {
EUR: {
'2': {
price_in_cents: 51600,
},
'3': {
price_in_cents: 77400,
},
'4': {
price_in_cents: 103200,
},
'5': {
price_in_cents: 129000,
},
'10': {
price_in_cents: 143000,
},
'20': {
price_in_cents: 264000,
},
},
USD: {
'2': {
price_in_cents: 55800,
},
'3': {
price_in_cents: 83700,
},
'4': {
price_in_cents: 111600,
},
'5': {
price_in_cents: 139500,
},
'10': {
price_in_cents: 155000,
},
'20': {
price_in_cents: 286000,
},
},
},
collaborator: {
EUR: {
'2': {
price_in_cents: 25000,
},
'3': {
price_in_cents: 37500,
},
'4': {
price_in_cents: 50000,
},
'5': {
price_in_cents: 62500,
},
'10': {
price_in_cents: 69000,
},
'20': {
price_in_cents: 128000,
},
},
USD: {
'2': {
price_in_cents: 27800,
},
'3': {
price_in_cents: 41700,
},
'4': {
price_in_cents: 55600,
},
'5': {
price_in_cents: 69500,
},
'10': {
price_in_cents: 77000,
},
'20': {
price_in_cents: 142000,
},
},
},
},
enterprise: {
professional: {
EUR: {
'2': {
price_in_cents: 51600,
},
'3': {
price_in_cents: 77400,
},
'4': {
price_in_cents: 103200,
},
'5': {
price_in_cents: 129000,
},
'10': {
price_in_cents: 239000,
},
'20': {
price_in_cents: 442000,
},
},
USD: {
'2': {
price_in_cents: 55800,
},
'3': {
price_in_cents: 83700,
},
'4': {
price_in_cents: 111600,
},
'5': {
price_in_cents: 139500,
},
'10': {
price_in_cents: 259000,
},
'20': {
price_in_cents: 478000,
},
},
},
collaborator: {
EUR: {
'2': {
price_in_cents: 25000,
},
'3': {
price_in_cents: 37500,
},
'4': {
price_in_cents: 50000,
},
'5': {
price_in_cents: 62500,
},
'10': {
price_in_cents: 116000,
},
'20': {
price_in_cents: 214000,
},
},
USD: {
'2': {
price_in_cents: 27800,
},
'3': {
price_in_cents: 41700,
},
'4': {
price_in_cents: 55600,
},
'5': {
price_in_cents: 69500,
},
'10': {
price_in_cents: 129000,
},
'20': {
price_in_cents: 238000,
},
},
},
},
}

View File

@@ -0,0 +1,503 @@
import {
CustomSubscription,
GroupSubscription,
PaidSubscription,
} from '../../../../../types/subscription/dashboard/subscription'
import dateformat from 'dateformat'
const today = new Date()
const oneYearFromToday = new Date().setFullYear(today.getFullYear() + 1)
const nextPaymentDueAt = dateformat(oneYearFromToday, 'dS mmmm yyyy')
const nextPaymentDueDate = dateformat(oneYearFromToday, 'dS mmmm yyyy')
const sevenDaysFromToday = new Date().setDate(today.getDate() + 7)
const sevenDaysFromTodayFormatted = dateformat(
sevenDaysFromToday,
'dS mmmm yyyy'
)
export const annualActiveSubscription: PaidSubscription = {
manager_ids: ['abc123'],
member_ids: [],
invited_emails: [],
groupPlan: false,
membersLimit: 0,
_id: 'def456',
admin_id: 'abc123',
teamInvites: [],
planCode: 'collaborator-annual',
recurlySubscription_id: 'ghi789',
plan: {
planCode: 'collaborator-annual',
name: 'Standard (Collaborator) Annual',
price_in_cents: 21900,
annual: true,
featureDescription: [],
},
payment: {
taxRate: 0,
billingDetailsLink: '/user/subscription/recurly/billing-details',
accountManagementLink: '/user/subscription/recurly/account-management',
additionalLicenses: 0,
totalLicenses: 0,
nextPaymentDueAt,
nextPaymentDueDate,
currency: 'USD',
state: 'active',
trialEndsAtFormatted: null,
trialEndsAt: null,
activeCoupons: [],
accountEmail: 'fake@example.com',
hasPastDueInvoice: false,
displayPrice: '$199.00',
planOnlyDisplayPrice: '',
addOns: [],
addOnDisplayPricesWithoutAdditionalLicense: {},
},
}
export const annualActiveSubscriptionEuro: PaidSubscription = {
manager_ids: ['abc123'],
member_ids: [],
invited_emails: [],
groupPlan: false,
membersLimit: 0,
_id: 'def456',
admin_id: 'abc123',
teamInvites: [],
planCode: 'collaborator-annual',
recurlySubscription_id: 'ghi789',
plan: {
planCode: 'collaborator-annual',
name: 'Standard (Collaborator) Annual',
price_in_cents: 21900,
annual: true,
featureDescription: [],
},
payment: {
taxRate: 0.24,
billingDetailsLink: '/user/subscription/recurly/billing-details',
accountManagementLink: '/user/subscription/recurly/account-management',
additionalLicenses: 0,
totalLicenses: 0,
nextPaymentDueAt,
nextPaymentDueDate,
currency: 'EUR',
state: 'active',
trialEndsAtFormatted: null,
trialEndsAt: null,
activeCoupons: [],
accountEmail: 'fake@example.com',
hasPastDueInvoice: false,
displayPrice: '€221.96',
planOnlyDisplayPrice: '',
addOns: [],
addOnDisplayPricesWithoutAdditionalLicense: {},
},
}
export const annualActiveSubscriptionPro: PaidSubscription = {
manager_ids: ['abc123'],
member_ids: [],
invited_emails: [],
groupPlan: false,
membersLimit: 0,
_id: 'def456',
admin_id: 'abc123',
teamInvites: [],
planCode: 'professional',
recurlySubscription_id: 'ghi789',
plan: {
planCode: 'professional',
name: 'Professional',
price_in_cents: 4500,
featureDescription: [],
},
payment: {
taxRate: 0,
billingDetailsLink: '/user/subscription/recurly/billing-details',
accountManagementLink: '/user/subscription/recurly/account-management',
additionalLicenses: 0,
totalLicenses: 0,
nextPaymentDueAt,
nextPaymentDueDate,
currency: 'USD',
state: 'active',
trialEndsAtFormatted: null,
trialEndsAt: null,
activeCoupons: [],
accountEmail: 'fake@example.com',
hasPastDueInvoice: false,
displayPrice: '$42.00',
planOnlyDisplayPrice: '',
addOns: [],
addOnDisplayPricesWithoutAdditionalLicense: {},
},
}
export const pastDueExpiredSubscription: PaidSubscription = {
manager_ids: ['abc123'],
member_ids: [],
invited_emails: [],
groupPlan: false,
membersLimit: 0,
_id: 'def456',
admin_id: 'abc123',
teamInvites: [],
planCode: 'collaborator-annual',
recurlySubscription_id: 'ghi789',
plan: {
planCode: 'collaborator-annual',
name: 'Standard (Collaborator) Annual',
price_in_cents: 21900,
annual: true,
featureDescription: [],
},
payment: {
taxRate: 0,
billingDetailsLink: '/user/subscription/recurly/billing-details',
accountManagementLink: '/user/subscription/recurly/account-management',
additionalLicenses: 0,
totalLicenses: 0,
nextPaymentDueAt,
nextPaymentDueDate,
currency: 'USD',
state: 'expired',
trialEndsAtFormatted: null,
trialEndsAt: null,
activeCoupons: [],
accountEmail: 'fake@example.com',
hasPastDueInvoice: true,
displayPrice: '$199.00',
planOnlyDisplayPrice: '',
addOns: [],
addOnDisplayPricesWithoutAdditionalLicense: {},
},
}
export const canceledSubscription: PaidSubscription = {
manager_ids: ['abc123'],
member_ids: [],
invited_emails: [],
groupPlan: false,
membersLimit: 0,
_id: 'def456',
admin_id: 'abc123',
teamInvites: [],
planCode: 'collaborator-annual',
recurlySubscription_id: 'ghi789',
plan: {
planCode: 'collaborator-annual',
name: 'Standard (Collaborator) Annual',
price_in_cents: 21900,
annual: true,
featureDescription: [],
},
payment: {
taxRate: 0,
billingDetailsLink: '/user/subscription/recurly/billing-details',
accountManagementLink: '/user/subscription/recurly/account-management',
additionalLicenses: 0,
totalLicenses: 0,
nextPaymentDueAt,
nextPaymentDueDate,
currency: 'USD',
state: 'canceled',
trialEndsAtFormatted: null,
trialEndsAt: null,
activeCoupons: [],
accountEmail: 'fake@example.com',
hasPastDueInvoice: false,
displayPrice: '$199.00',
planOnlyDisplayPrice: '',
addOns: [],
addOnDisplayPricesWithoutAdditionalLicense: {},
},
}
export const pendingSubscriptionChange: PaidSubscription = {
manager_ids: ['abc123'],
member_ids: [],
invited_emails: [],
groupPlan: false,
membersLimit: 0,
_id: 'def456',
admin_id: 'abc123',
teamInvites: [],
planCode: 'collaborator-annual',
recurlySubscription_id: 'ghi789',
plan: {
planCode: 'collaborator-annual',
name: 'Standard (Collaborator) Annual',
price_in_cents: 21900,
annual: true,
featureDescription: [],
},
payment: {
taxRate: 0,
billingDetailsLink: '/user/subscription/recurly/billing-details',
accountManagementLink: '/user/subscription/recurly/account-management',
additionalLicenses: 0,
totalLicenses: 0,
nextPaymentDueAt,
nextPaymentDueDate,
currency: 'USD',
state: 'active',
trialEndsAtFormatted: null,
trialEndsAt: null,
activeCoupons: [],
accountEmail: 'fake@example.com',
hasPastDueInvoice: false,
displayPrice: '$199.00',
planOnlyDisplayPrice: '',
addOns: [],
addOnDisplayPricesWithoutAdditionalLicense: {},
},
pendingPlan: {
planCode: 'professional-annual',
name: 'Professional Annual',
price_in_cents: 42900,
annual: true,
featureDescription: [],
},
}
export const groupActiveSubscription: GroupSubscription = {
manager_ids: ['abc123'],
member_ids: ['abc123'],
invited_emails: [],
groupPlan: true,
teamName: 'GAS',
membersLimit: 10,
_id: 'bcd567',
admin_id: 'abc123',
teamInvites: [],
planCode: 'group_collaborator_10_enterprise',
recurlySubscription_id: 'ghi789',
plan: {
planCode: 'group_collaborator_10_enterprise',
name: 'Overleaf Standard (Collaborator) - Group Account (10 licenses) - Enterprise',
hideFromUsers: true,
price_in_cents: 129000,
annual: true,
groupPlan: true,
membersLimit: 10,
membersLimitAddOn: 'additional-license',
},
payment: {
taxRate: 0,
billingDetailsLink: '/user/subscription/recurly/billing-details',
accountManagementLink: '/user/subscription/recurly/account-management',
additionalLicenses: 0,
totalLicenses: 10,
nextPaymentDueAt,
nextPaymentDueDate,
currency: 'USD',
state: 'active',
trialEndsAtFormatted: null,
trialEndsAt: null,
activeCoupons: [],
accountEmail: 'fake@example.com',
hasPastDueInvoice: false,
displayPrice: '$1290.00',
planOnlyDisplayPrice: '',
addOns: [],
addOnDisplayPricesWithoutAdditionalLicense: {},
},
}
export const groupActiveSubscriptionWithPendingLicenseChange: GroupSubscription =
{
manager_ids: ['abc123'],
member_ids: ['abc123'],
invited_emails: [],
groupPlan: true,
teamName: 'GASWPLC',
membersLimit: 10,
_id: 'def456',
admin_id: 'abc123',
teamInvites: [],
planCode: 'group_collaborator_10_enterprise',
recurlySubscription_id: 'ghi789',
plan: {
planCode: 'group_collaborator_10_enterprise',
name: 'Overleaf Standard (Collaborator) - Group Account (10 licenses) - Enterprise',
hideFromUsers: true,
price_in_cents: 129000,
annual: true,
groupPlan: true,
membersLimit: 10,
membersLimitAddOn: 'additional-license',
},
payment: {
taxRate: 0,
billingDetailsLink: '/user/subscription/recurly/billing-details',
accountManagementLink: '/user/subscription/recurly/account-management',
additionalLicenses: 11,
totalLicenses: 21,
nextPaymentDueAt,
nextPaymentDueDate,
currency: 'USD',
state: 'active',
trialEndsAtFormatted: null,
trialEndsAt: null,
activeCoupons: [],
accountEmail: 'fake@example.com',
hasPastDueInvoice: false,
displayPrice: '$2967.00',
pendingAdditionalLicenses: 13,
pendingTotalLicenses: 23,
planOnlyDisplayPrice: '',
addOns: [],
addOnDisplayPricesWithoutAdditionalLicense: {},
},
pendingPlan: {
planCode: 'group_collaborator_10_enterprise',
name: 'Overleaf Standard (Collaborator) - Group Account (10 licenses) - Enterprise',
hideFromUsers: true,
price_in_cents: 129000,
annual: true,
groupPlan: true,
membersLimit: 10,
membersLimitAddOn: 'additional-license',
},
}
export const trialSubscription: PaidSubscription = {
manager_ids: ['abc123'],
member_ids: [],
invited_emails: [],
groupPlan: false,
membersLimit: 0,
_id: 'def456',
admin_id: 'abc123',
teamInvites: [],
planCode: 'paid-personal_free_trial_7_days',
recurlySubscription_id: 'ghi789',
plan: {
planCode: 'paid-personal_free_trial_7_days',
name: 'Personal',
price_in_cents: 1500,
featureDescription: [],
hideFromUsers: true,
},
payment: {
taxRate: 0,
billingDetailsLink: '/user/subscription/recurly/billing-details',
accountManagementLink: '/user/subscription/recurly/account-management',
additionalLicenses: 0,
totalLicenses: 0,
nextPaymentDueAt: sevenDaysFromTodayFormatted,
nextPaymentDueDate: sevenDaysFromTodayFormatted,
currency: 'USD',
state: 'active',
trialEndsAtFormatted: sevenDaysFromTodayFormatted,
trialEndsAt: new Date(sevenDaysFromToday).toString(),
activeCoupons: [],
accountEmail: 'fake@example.com',
hasPastDueInvoice: false,
displayPrice: '$14.00',
planOnlyDisplayPrice: '',
addOns: [],
addOnDisplayPricesWithoutAdditionalLicense: {},
},
}
export const customSubscription: CustomSubscription = {
manager_ids: ['abc123'],
member_ids: [],
invited_emails: [],
groupPlan: false,
membersLimit: 0,
_id: 'def456',
admin_id: 'abc123',
teamInvites: [],
planCode: 'collaborator-annual',
recurlySubscription_id: 'ghi789',
plan: {
planCode: 'collaborator-annual',
name: 'Standard (Collaborator) Annual',
price_in_cents: 21900,
annual: true,
featureDescription: [],
},
customAccount: true,
}
export const trialCollaboratorSubscription: PaidSubscription = {
manager_ids: ['abc123'],
member_ids: [],
invited_emails: [],
groupPlan: false,
membersLimit: 0,
_id: 'def456',
admin_id: 'abc123',
teamInvites: [],
planCode: 'collaborator_free_trial_7_days',
recurlySubscription_id: 'ghi789',
plan: {
planCode: 'collaborator_free_trial_7_days',
name: 'Standard (Collaborator)',
price_in_cents: 2300,
featureDescription: [],
hideFromUsers: true,
},
payment: {
taxRate: 0,
billingDetailsLink: '/user/subscription/recurly/billing-details',
accountManagementLink: '/user/subscription/recurly/account-management',
additionalLicenses: 0,
totalLicenses: 0,
nextPaymentDueAt: sevenDaysFromTodayFormatted,
nextPaymentDueDate: sevenDaysFromTodayFormatted,
currency: 'USD',
state: 'active',
trialEndsAtFormatted: sevenDaysFromTodayFormatted,
trialEndsAt: new Date(sevenDaysFromToday).toString(),
activeCoupons: [],
accountEmail: 'foo@example.com',
hasPastDueInvoice: false,
displayPrice: '$21.00',
planOnlyDisplayPrice: '',
addOns: [],
addOnDisplayPricesWithoutAdditionalLicense: {},
},
}
export const monthlyActiveCollaborator: PaidSubscription = {
manager_ids: ['abc123'],
member_ids: [],
invited_emails: [],
groupPlan: false,
membersLimit: 0,
_id: 'def456',
admin_id: 'abc123',
teamInvites: [],
planCode: 'collaborator',
recurlySubscription_id: 'ghi789',
plan: {
planCode: 'collaborator',
name: 'Standard (Collaborator)',
price_in_cents: 212300900,
featureDescription: [],
},
payment: {
taxRate: 0,
billingDetailsLink: '/user/subscription/recurly/billing-details',
accountManagementLink: '/user/subscription/recurly/account-management',
additionalLicenses: 0,
totalLicenses: 0,
nextPaymentDueAt,
nextPaymentDueDate,
currency: 'USD',
state: 'active',
trialEndsAtFormatted: null,
trialEndsAt: null,
activeCoupons: [],
accountEmail: 'foo@example.com',
hasPastDueInvoice: false,
displayPrice: '$21.00',
planOnlyDisplayPrice: '',
addOns: [],
addOnDisplayPricesWithoutAdditionalLicense: {},
},
}

View File

@@ -0,0 +1,32 @@
import { ActiveSubscription } from '../../../../../frontend/js/features/subscription/components/dashboard/states/active/active'
import { PaidSubscription } from '../../../../../types/subscription/dashboard/subscription'
import { groupPlans, plans } from '../fixtures/plans'
import { renderWithSubscriptionDashContext } from './render-with-subscription-dash-context'
import { MetaTag } from '@/utils/meta'
import { CurrencyCode } from '../../../../../types/subscription/currency'
export function renderActiveSubscription(
subscription: PaidSubscription,
tags: MetaTag[] = [],
currencyCode?: CurrencyCode
) {
renderWithSubscriptionDashContext(
<ActiveSubscription subscription={subscription} />,
{
currencyCode,
metaTags: [
...tags,
{ name: 'ol-plans', value: plans },
{
name: 'ol-groupPlans',
value: groupPlans,
},
{ name: 'ol-subscription', value: subscription },
{
name: 'ol-recommendedCurrency',
value: currencyCode || 'USD',
},
],
}
)
}

View File

@@ -0,0 +1,94 @@
import { render } from '@testing-library/react'
import _ from 'lodash'
import { SubscriptionDashboardProvider } from '../../../../../frontend/js/features/subscription/context/subscription-dashboard-context'
import { groupPriceByUsageTypeAndSize, plans } from '../fixtures/plans'
import fetchMock from 'fetch-mock'
import { SplitTestProvider } from '@/shared/context/split-test-context'
import { MetaTag } from '@/utils/meta'
export function renderWithSubscriptionDashContext(
component: React.ReactElement,
options?: {
metaTags?: MetaTag[]
recurlyNotLoaded?: boolean
queryingRecurly?: boolean
currencyCode?: string
}
) {
const SubscriptionDashboardProviderWrapper = ({
children,
}: {
children: React.ReactNode
}) => (
<SplitTestProvider>
<SubscriptionDashboardProvider>{children}</SubscriptionDashboardProvider>
</SplitTestProvider>
)
options?.metaTags?.forEach(tag =>
window.metaAttributesCache.set(tag!.name, tag!.value)
)
window.metaAttributesCache.set('ol-user', {})
if (!options?.recurlyNotLoaded) {
// @ts-ignore
global.recurly = {
configure: () => {},
Pricing: {
Subscription: () => {
return {
plan: (planCode: string) => {
let plan
const isGroupPlan = planCode.includes('group')
if (isGroupPlan) {
const [, planType, size, usage] = planCode.split('_')
const currencyCode = options?.currencyCode || 'USD'
plan = _.get(groupPriceByUsageTypeAndSize, [
usage,
planType,
currencyCode,
size,
])
} else {
plan = plans.find(p => p.planCode === planCode)
}
const response = {
next: {
total: plan?.price_in_cents
? plan.price_in_cents / 100
: undefined,
},
}
return {
currency: () => {
return {
catch: () => {
return {
done: (callback: (response: object) => void) => {
if (!options?.queryingRecurly) {
return callback(response)
}
},
}
},
}
},
}
},
}
},
},
}
}
return render(component, {
wrapper: SubscriptionDashboardProviderWrapper,
})
}
export function cleanUpContext() {
// @ts-ignore
delete global.recurly
fetchMock.removeRoutes().clearHistory()
}

View File

@@ -0,0 +1,24 @@
import { expect } from 'chai'
import isInFreeTrial from '../../../../../frontend/js/features/subscription/util/is-in-free-trial'
import dateformat from 'dateformat'
describe('isInFreeTrial', function () {
it('returns false when no date sent', function () {
expect(isInFreeTrial()).to.be.false
})
it('returns false when date is null', function () {
expect(isInFreeTrial(null)).to.be.false
})
it('returns false when date is in the past', function () {
expect(isInFreeTrial('2000-02-16T17:59:07.000Z')).to.be.false
})
it('returns true when date is in the future', function () {
const today = new Date()
const sevenDaysFromToday = new Date().setDate(today.getDate() + 7)
const sevenDaysFromTodayFormatted = dateformat(
sevenDaysFromToday,
'dS mmmm yyyy'
)
expect(isInFreeTrial(sevenDaysFromTodayFormatted)).to.be.true
})
})

View File

@@ -0,0 +1,17 @@
import { expect } from 'chai'
import isMonthlyCollaboratorPlan from '../../../../../frontend/js/features/subscription/util/is-monthly-collaborator-plan'
describe('isMonthlyCollaboratorPlan', function () {
it('returns false when a plan code without "collaborator" ', function () {
expect(isMonthlyCollaboratorPlan('test', false)).to.be.false
})
it('returns false when on a plan with "collaborator" and "ann"', function () {
expect(isMonthlyCollaboratorPlan('collaborator-annual', false)).to.be.false
})
it('returns false when on a plan with "collaborator" and without "ann" but is a group plan', function () {
expect(isMonthlyCollaboratorPlan('collaborator', true)).to.be.false
})
it('returns true when on a plan with non-group "collaborator" monthly plan', function () {
expect(isMonthlyCollaboratorPlan('collaborator', false)).to.be.true
})
})

View File

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

View File

@@ -0,0 +1,68 @@
import { expect } from 'chai'
import showDowngradeOption from '../../../../../frontend/js/features/subscription/util/show-downgrade-option'
import dateformat from 'dateformat'
describe('showDowngradeOption', function () {
const today = new Date()
const sevenDaysFromToday = new Date().setDate(today.getDate() + 7)
const sevenDaysFromTodayFormatted = dateformat(
sevenDaysFromToday,
'dS mmmm yyyy'
)
it('returns false when no trial end date', function () {
expect(showDowngradeOption('collab')).to.be.false
})
it('returns false when a plan code without "collaborator" ', function () {
expect(showDowngradeOption('test', false, sevenDaysFromTodayFormatted)).to
.be.false
})
it('returns false when on a plan with trial date in future but has "collaborator" and "ann" in plan code', function () {
expect(
showDowngradeOption(
'collaborator-annual',
false,
sevenDaysFromTodayFormatted
)
).to.be.false
})
it('returns false when on a plan with trial date in future and plan code has "collaborator" and no "ann" but is a group plan', function () {
expect(
showDowngradeOption('collaborator', true, sevenDaysFromTodayFormatted)
).to.be.false
})
it('returns false when on a plan with "collaborator" and without "ann" and trial date in future', function () {
expect(
showDowngradeOption('collaborator', false, sevenDaysFromTodayFormatted)
).to.be.false
})
it('returns true when on a plan with "collaborator" and without "ann" and no trial date', function () {
expect(showDowngradeOption('collaborator', false)).to.be.true
})
it('returns true when on a plan with "collaborator" and without "ann" and trial date is in the past', function () {
expect(
showDowngradeOption('collaborator', false, '2000-02-16T17:59:07.000Z')
).to.be.true
})
it('returns false when on a monthly collaborator plan with a pending pause', function () {
expect(
showDowngradeOption(
'collaborator',
false,
null,
'2030-01-01T12:00:00.000Z'
)
).to.be.false
})
it('returns false when on a monthly collaborator plan with an active pause', function () {
expect(
showDowngradeOption(
'collaborator',
false,
null,
'2030-01-01T12:00:00.000Z',
5
)
).to.be.false
})
})