first commit
This commit is contained in:
@@ -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')
|
||||
})
|
||||
})
|
Reference in New Issue
Block a user