first commit
This commit is contained in:
@@ -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')
|
||||
})
|
||||
})
|
@@ -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')
|
||||
})
|
||||
})
|
@@ -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)
|
||||
})
|
||||
})
|
@@ -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.'
|
||||
)
|
||||
})
|
||||
})
|
@@ -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
|
||||
})
|
||||
})
|
@@ -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)
|
||||
})
|
||||
})
|
@@ -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)
|
||||
})
|
||||
})
|
@@ -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('We’d love you to stay')
|
||||
})
|
||||
|
||||
it('does not render with a group plan', async function () {
|
||||
renderSubscriptionWithPauseSupport(groupActiveSubscription)
|
||||
clickCancelButton()
|
||||
// goes straight to cancel
|
||||
await screen.findByText('We’d love you to stay')
|
||||
})
|
||||
|
||||
it('does not render when in a trial', async function () {
|
||||
renderSubscriptionWithPauseSupport(trialSubscription)
|
||||
clickCancelButton()
|
||||
await screen.findByText('We’d 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
|
||||
})
|
||||
})
|
||||
})
|
@@ -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
|
||||
})
|
||||
})
|
@@ -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('You’re 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('We’d 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 = 'I’ll 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,
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
@@ -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,
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
@@ -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,
|
||||
})
|
||||
})
|
||||
})
|
@@ -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(
|
||||
`You’re 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(`You’re 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')
|
||||
})
|
||||
})
|
@@ -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')
|
||||
})
|
||||
})
|
@@ -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 can’t join this group subscription')
|
||||
screen.getByText(
|
||||
'Your Overleaf account is managed by your current group admin (example@overleaf.com). This means you can’t 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 can’t join this group subscription')
|
||||
screen.getByText(
|
||||
'Your Overleaf account is managed by your current group admin (example@overleaf.com). This means you can’t 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'
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
@@ -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.'
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
@@ -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')
|
||||
})
|
||||
})
|
||||
})
|
@@ -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 can’t join additional group subscriptions',
|
||||
{ exact: false }
|
||||
)
|
||||
screen.getByRole('link', { name: 'Read more about Managed Users.' })
|
||||
})
|
||||
})
|
@@ -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(
|
||||
/it’s 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')
|
||||
})
|
||||
})
|
@@ -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,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
@@ -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: {},
|
||||
},
|
||||
}
|
@@ -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',
|
||||
},
|
||||
],
|
||||
}
|
||||
)
|
||||
}
|
@@ -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()
|
||||
}
|
@@ -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
|
||||
})
|
||||
})
|
@@ -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
|
||||
})
|
||||
})
|
@@ -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,
|
||||
})
|
||||
})
|
||||
})
|
@@ -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
|
||||
})
|
||||
})
|
Reference in New Issue
Block a user