import moment from 'moment-timezone';
import {OrganizationFixture} from 'sentry-fixture/organization';
import {RouteComponentPropsFixture} from 'sentry-fixture/routeComponentPropsFixture';
import {BillingConfigFixture} from 'getsentry-test/fixtures/billingConfig';
import {MetricHistoryFixture} from 'getsentry-test/fixtures/metricHistory';
import {SubscriptionFixture} from 'getsentry-test/fixtures/subscription';
import {
act,
render,
screen,
userEvent,
waitFor,
within,
} from 'sentry-test/reactTestingLibrary';
import SubscriptionStore from 'getsentry/stores/subscriptionStore';
import type {Subscription as SubscriptionType} from 'getsentry/types';
import {OnDemandBudgetMode, PlanTier} from 'getsentry/types';
import AMCheckout from 'getsentry/views/amCheckout';
import {getCheckoutAPIData} from 'getsentry/views/amCheckout/utils';
import {hasOnDemandBudgetsFeature} from 'getsentry/views/onDemandBudgets/utils';
describe('AM1 Checkout', function () {
let mockResponse: any;
const api = new MockApiClient();
const organization = OrganizationFixture({features: []});
const subscription = SubscriptionFixture({organization});
const params = {};
beforeEach(function () {
SubscriptionStore.set(organization.slug, subscription);
MockApiClient.clearMockResponses();
MockApiClient.addMockResponse({
url: `/subscriptions/${organization.slug}/`,
method: 'GET',
body: {},
});
mockResponse = MockApiClient.addMockResponse({
url: `/customers/${organization.slug}/billing-config/`,
method: 'GET',
body: BillingConfigFixture(PlanTier.AM2),
});
MockApiClient.addMockResponse({
url: `/organizations/${organization.slug}/promotions/trigger-check/`,
method: 'POST',
body: {},
});
MockApiClient.addMockResponse({
url: `/customers/${organization.slug}/plan-migrations/?applied=0`,
method: 'GET',
body: {},
});
});
it('renders', async function () {
render(
,
{organization}
);
expect(
await screen.findByRole('heading', {name: 'Change Subscription'})
).toBeInTheDocument();
expect(screen.getByTestId('checkout-steps')).toBeInTheDocument();
expect(screen.getByRole('radio', {name: 'Business'})).toBeInTheDocument();
await waitFor(() => {
expect(mockResponse).toHaveBeenCalledWith(
`/customers/${organization.slug}/billing-config/`,
expect.objectContaining({
method: 'GET',
data: {tier: 'am1'},
})
);
});
});
it('can skip to step and continue', async function () {
render(
,
{organization}
);
expect(
await screen.findByRole('heading', {name: 'Change Subscription'})
).toBeInTheDocument();
await userEvent.click(screen.getByText('Reserved Volumes'));
await userEvent.click(screen.getByRole('button', {name: 'Continue'}));
// Both steps are complete
expect(
within(screen.getByTestId('header-choose-your-plan')).getByTestId('icon-check-mark')
).toBeInTheDocument();
expect(
within(screen.getByTestId('header-reserved-volumes')).getByTestId('icon-check-mark')
).toBeInTheDocument();
});
it('renders cancel subscription button', async function () {
const sub: SubscriptionType = {...subscription, canCancel: true};
SubscriptionStore.set(organization.slug, sub);
render(
,
{organization}
);
expect(
await screen.findByRole('heading', {name: 'Change Subscription'})
).toBeInTheDocument();
expect(screen.getByRole('button', {name: 'Cancel Subscription'})).toBeInTheDocument();
});
it('renders pending cancellation button', async function () {
const sub: SubscriptionType = {
...subscription,
canCancel: true,
cancelAtPeriodEnd: true,
};
SubscriptionStore.set(organization.slug, sub);
render(
,
{organization}
);
expect(
await screen.findByRole('heading', {name: 'Change Subscription'})
).toBeInTheDocument();
expect(await screen.findByText('Pending Cancellation')).toBeInTheDocument();
});
it('does not renders cancel subscription button if cannot cancel', async function () {
render(
,
{organization}
);
expect(
await screen.findByRole('heading', {name: 'Change Subscription'})
).toBeInTheDocument();
expect(
screen.queryByRole('button', {name: 'Cancel Subscription'})
).not.toBeInTheDocument();
});
it('renders annual terms for annual plan', async function () {
const sub: SubscriptionType = {
...subscription,
plan: 'am1_team_auf',
contractInterval: 'annual',
billingInterval: 'annual',
};
SubscriptionStore.set(organization.slug, sub);
const {container} = render(
,
{organization}
);
expect(
await screen.findByRole('heading', {name: 'Change Subscription'})
).toBeInTheDocument();
expect(container).toHaveTextContent(
'Annual subscriptions require a one-year non-cancellable commitment'
);
});
it('does not render annual terms for monthly plan', async function () {
const sub = {...subscription};
SubscriptionStore.set(organization.slug, sub);
const {container} = render(
,
{organization}
);
expect(
await screen.findByRole('heading', {name: 'Change Subscription'})
).toBeInTheDocument();
expect(container).not.toHaveTextContent(
'Annual subscriptions require a one-year non-cancellable commitment'
);
});
it('renders default plan data', async function () {
render(
,
{organization}
);
expect(
await screen.findByRole('heading', {name: 'Change Subscription'})
).toBeInTheDocument();
expect(screen.getByRole('radio', {name: 'Business'})).toBeChecked();
await userEvent.click(screen.getByText('Reserved Volumes'));
// TODO: Can better write this once we have
// https://github.com/testing-library/jest-dom/issues/478
expect(screen.getByRole('slider', {name: 'Errors'})).toHaveAttribute(
'aria-valuetext',
'50000'
);
expect(screen.getByRole('slider', {name: 'Performance units'})).toHaveAttribute(
'aria-valuetext',
'100000'
);
expect(screen.getByRole('slider', {name: 'Replays'})).toHaveAttribute(
'aria-valuetext',
'500'
);
expect(screen.getByRole('slider', {name: 'Attachments'})).toHaveAttribute(
'aria-valuetext',
'1'
);
await userEvent.click(screen.getByText('On-Demand Max Spend'));
expect(screen.getByRole('textbox', {name: 'Monthly Max'})).toHaveValue('');
});
it('prefills with am1 team subscription data', async function () {
const sub: SubscriptionType = SubscriptionFixture({
organization,
plan: 'am1_business',
planTier: 'am1',
categories: {
errors: MetricHistoryFixture({reserved: 200000}),
transactions: MetricHistoryFixture({reserved: 250000}),
replays: MetricHistoryFixture({reserved: 10_000}),
attachments: MetricHistoryFixture({reserved: 25}),
monitorSeats: MetricHistoryFixture({reserved: 1}),
},
onDemandMaxSpend: 10000,
});
SubscriptionStore.set(organization.slug, sub);
render(
,
{
organization,
}
);
expect(
await screen.findByRole('heading', {name: 'Change Subscription'})
).toBeInTheDocument();
expect(screen.getByRole('radio', {name: 'Business'})).toBeChecked();
await userEvent.click(screen.getByText('Reserved Volumes'));
// TODO: Can better write this once we have
// https://github.com/testing-library/jest-dom/issues/478
expect(screen.getByRole('slider', {name: 'Errors'})).toHaveAttribute(
'aria-valuetext',
'200000'
);
expect(screen.getByRole('slider', {name: 'Performance units'})).toHaveAttribute(
'aria-valuetext',
'250000'
);
expect(screen.getByRole('slider', {name: 'Replays'})).toHaveAttribute(
'aria-valuetext',
'10000'
);
expect(screen.getByRole('slider', {name: 'Attachments'})).toHaveAttribute(
'aria-valuetext',
'25'
);
await userEvent.click(screen.getByText('On-Demand Max Spend'));
expect(screen.getByRole('textbox', {name: 'Monthly Max'})).toHaveValue('100');
});
it('prefills with am1 business subscription data', async function () {
const sub = SubscriptionFixture({
organization,
plan: 'am1_business',
planTier: 'am1',
categories: {
errors: MetricHistoryFixture({reserved: 50000}),
transactions: MetricHistoryFixture({reserved: 250000}),
replays: MetricHistoryFixture({reserved: 500}),
attachments: MetricHistoryFixture({reserved: 50}),
monitorSeats: MetricHistoryFixture({reserved: 1}),
},
onDemandMaxSpend: 10000,
});
SubscriptionStore.set(organization.slug, sub);
render(
,
{
organization,
}
);
expect(
await screen.findByRole('heading', {name: 'Change Subscription'})
).toBeInTheDocument();
expect(screen.getByRole('radio', {name: 'Business'})).toBeChecked();
await userEvent.click(screen.getByText('Reserved Volumes'));
// TODO: Can better write this once we have
// https://github.com/testing-library/jest-dom/issues/478
expect(screen.getByRole('slider', {name: 'Errors'})).toHaveAttribute(
'aria-valuetext',
'50000'
);
expect(screen.getByRole('slider', {name: 'Performance units'})).toHaveAttribute(
'aria-valuetext',
'250000'
);
expect(screen.getByRole('slider', {name: 'Replays'})).toHaveAttribute(
'aria-valuetext',
'500'
);
expect(screen.getByRole('slider', {name: 'Attachments'})).toHaveAttribute(
'aria-valuetext',
'50'
);
await userEvent.click(screen.getByText('On-Demand Max Spend'));
expect(screen.getByRole('textbox', {name: 'Monthly Max'})).toHaveValue('100');
});
it('prefills with mm2 team subscription data', async function () {
const sub = SubscriptionFixture({
organization,
plan: 'mm2_b_100k',
planTier: 'mm2',
categories: {errors: MetricHistoryFixture({reserved: 100000})},
onDemandMaxSpend: 2000,
});
SubscriptionStore.set(organization.slug, sub);
render(
,
{
organization,
}
);
expect(
await screen.findByRole('heading', {name: 'Change Subscription'})
).toBeInTheDocument();
expect(screen.getByRole('radio', {name: 'Business'})).toBeChecked();
await userEvent.click(screen.getByText('Reserved Volumes'));
// TODO: Can better write this once we have
// https://github.com/testing-library/jest-dom/issues/478
expect(screen.getByRole('slider', {name: 'Errors'})).toHaveAttribute(
'aria-valuetext',
'100000'
);
expect(screen.getByRole('slider', {name: 'Performance units'})).toHaveAttribute(
'aria-valuetext',
'100000'
);
expect(screen.getByRole('slider', {name: 'Attachments'})).toHaveAttribute(
'aria-valuetext',
'1'
);
await userEvent.click(screen.getByText('On-Demand Max Spend'));
expect(screen.getByRole('textbox', {name: 'Monthly Max'})).toHaveValue('20');
});
it('prefills with mm2 biz subscription data', async function () {
const sub = SubscriptionFixture({
organization,
plan: 'mm2_a_100k',
planTier: 'mm2',
categories: {errors: MetricHistoryFixture({reserved: 100_000})},
onDemandMaxSpend: 2000,
});
SubscriptionStore.set(organization.slug, sub);
render(
);
expect(
await screen.findByRole('heading', {name: 'Change Subscription'})
).toBeInTheDocument();
expect(screen.getByRole('radio', {name: 'Business'})).toBeChecked();
await userEvent.click(screen.getByText('Reserved Volumes'));
// TODO: Can better write this once we have
// https://github.com/testing-library/jest-dom/issues/478
expect(screen.getByRole('slider', {name: 'Errors'})).toHaveAttribute(
'aria-valuetext',
'100000'
);
expect(screen.getByRole('slider', {name: 'Performance units'})).toHaveAttribute(
'aria-valuetext',
'100000'
);
expect(screen.getByRole('slider', {name: 'Attachments'})).toHaveAttribute(
'aria-valuetext',
'1'
);
await userEvent.click(screen.getByText('On-Demand Max Spend'));
expect(screen.getByRole('textbox', {name: 'Monthly Max'})).toHaveValue('20');
});
it('prefills with s1 subscription data', async function () {
const sub = SubscriptionFixture({
organization,
plan: 's1',
planTier: 'mm1',
categories: {errors: MetricHistoryFixture({reserved: 100000})},
onDemandMaxSpend: 2000,
});
SubscriptionStore.set(organization.slug, sub);
render(
,
{
organization,
}
);
expect(
await screen.findByRole('heading', {name: 'Change Subscription'})
).toBeInTheDocument();
expect(screen.getByRole('radio', {name: 'Business'})).toBeChecked();
await userEvent.click(screen.getByText('Reserved Volumes'));
// TODO: Can better write this once we have
// https://github.com/testing-library/jest-dom/issues/478
expect(screen.getByRole('slider', {name: 'Errors'})).toHaveAttribute(
'aria-valuetext',
'100000'
);
expect(screen.getByRole('slider', {name: 'Performance units'})).toHaveAttribute(
'aria-valuetext',
'100000'
);
expect(screen.getByRole('slider', {name: 'Attachments'})).toHaveAttribute(
'aria-valuetext',
'1'
);
await userEvent.click(screen.getByText('On-Demand Max Spend'));
expect(screen.getByRole('textbox', {name: 'Monthly Max'})).toHaveValue('20');
});
it('prefills with l1 subscription data', async function () {
const sub = SubscriptionFixture({
organization,
plan: 'l1',
planTier: 'mm1',
categories: {errors: MetricHistoryFixture({reserved: 100000})},
onDemandMaxSpend: 2000,
});
SubscriptionStore.set(organization.slug, sub);
render(
,
{
organization,
}
);
expect(
await screen.findByRole('heading', {name: 'Change Subscription'})
).toBeInTheDocument();
expect(screen.getByRole('radio', {name: 'Business'})).toBeChecked();
await userEvent.click(screen.getByText('Reserved Volumes'));
// TODO: Can better write this once we have
// https://github.com/testing-library/jest-dom/issues/478
expect(screen.getByRole('slider', {name: 'Errors'})).toHaveAttribute(
'aria-valuetext',
'100000'
);
expect(screen.getByRole('slider', {name: 'Performance units'})).toHaveAttribute(
'aria-valuetext',
'100000'
);
expect(screen.getByRole('slider', {name: 'Attachments'})).toHaveAttribute(
'aria-valuetext',
'1'
);
await userEvent.click(screen.getByText('On-Demand Max Spend'));
expect(screen.getByRole('textbox', {name: 'Monthly Max'})).toHaveValue('20');
});
it('handles subscription with unlimited ondemand', async function () {
const sub = {...subscription, onDemandMaxSpend: -1};
SubscriptionStore.set(organization.slug, sub);
render(
,
{
organization,
}
);
expect(
await screen.findByRole('heading', {name: 'Change Subscription'})
).toBeInTheDocument();
await userEvent.click(screen.getByText('On-Demand Max Spend'));
expect(screen.getByRole('textbox', {name: 'Monthly Max'})).toHaveValue('');
});
});
describe('AM2 Checkout', function () {
let mockResponse: any;
const api = new MockApiClient();
const organization = OrganizationFixture();
const subscription = SubscriptionFixture({organization});
const params = {};
beforeEach(function () {
SubscriptionStore.set(organization.slug, subscription);
MockApiClient.clearMockResponses();
MockApiClient.addMockResponse({
url: `/subscriptions/${organization.slug}/`,
method: 'GET',
body: {},
});
mockResponse = MockApiClient.addMockResponse({
url: `/customers/${organization.slug}/billing-config/`,
method: 'GET',
body: BillingConfigFixture(PlanTier.AM2),
});
MockApiClient.addMockResponse({
url: `/organizations/${organization.slug}/promotions/trigger-check/`,
method: 'POST',
});
MockApiClient.addMockResponse({
url: `/customers/${organization.slug}/plan-migrations/?applied=0`,
method: 'GET',
body: {},
});
});
it('renders for am1 team plan', async function () {
const sub = SubscriptionFixture({organization, plan: 'am1_team'});
SubscriptionStore.set(organization.slug, sub);
render(
,
{organization}
);
expect(
await screen.findByRole('heading', {name: 'Change Subscription'})
).toBeInTheDocument();
expect(screen.getByText('Choose Your Plan')).toBeInTheDocument();
expect(screen.getByRole('radio', {name: 'Business'})).toBeInTheDocument();
expect(screen.getByText('Cross-project visibility')).toBeInTheDocument();
expect(screen.getByRole('radio', {name: 'Team'})).toBeInTheDocument();
expect(screen.getByText('Unlimited members')).toBeInTheDocument();
expect(mockResponse).toHaveBeenCalledWith(
`/customers/${organization.slug}/billing-config/`,
expect.objectContaining({
method: 'GET',
data: {tier: PlanTier.AM2},
})
);
});
it('renders for am2 free plan', async function () {
const sub = SubscriptionFixture({organization, plan: 'am2_f'});
SubscriptionStore.set(organization.slug, sub);
render(
,
{organization}
);
expect(
await screen.findByRole('heading', {name: 'Change Subscription'})
).toBeInTheDocument();
expect(screen.getByText('Choose Your Plan')).toBeInTheDocument();
expect(screen.getByRole('radio', {name: 'Business'})).toBeInTheDocument();
expect(screen.getByText('Cross-project visibility')).toBeInTheDocument();
expect(screen.getByRole('radio', {name: 'Team'})).toBeInTheDocument();
expect(screen.getByText('Unlimited members')).toBeInTheDocument();
expect(mockResponse).toHaveBeenCalledWith(
`/customers/${organization.slug}/billing-config/`,
expect.objectContaining({
method: 'GET',
data: {tier: PlanTier.AM2},
})
);
});
it('prefills subscription data based on price with same plan type', async function () {
const sub = SubscriptionFixture({
organization,
plan: 'am1_business',
planTier: 'am1',
categories: {
errors: MetricHistoryFixture({reserved: 50_000}),
transactions: MetricHistoryFixture({reserved: 20_000_000}),
replays: MetricHistoryFixture({reserved: 500}),
attachments: MetricHistoryFixture({reserved: 1}),
monitorSeats: MetricHistoryFixture({reserved: 1}),
},
onDemandMaxSpend: 2000,
});
SubscriptionStore.set(organization.slug, sub);
render(
,
{organization}
);
expect(
await screen.findByRole('heading', {name: 'Change Subscription'})
).toBeInTheDocument();
expect(screen.getByRole('radio', {name: 'Business'})).toBeChecked();
await userEvent.click(screen.getByText('Reserved Volumes'));
// TODO: Can better write this once we have
// https://github.com/testing-library/jest-dom/issues/478
expect(screen.getByRole('slider', {name: 'Errors'})).toHaveAttribute(
'aria-valuetext',
'50000'
);
expect(screen.getByRole('slider', {name: 'Performance units'})).toHaveAttribute(
'aria-valuetext',
'35000000'
);
expect(screen.getByRole('slider', {name: 'Replays'})).toHaveAttribute(
'aria-valuetext',
'500'
);
expect(screen.getByRole('slider', {name: 'Attachments'})).toHaveAttribute(
'aria-valuetext',
'1'
);
await userEvent.click(screen.getByText('On-Demand Max Spend'));
expect(screen.getByRole('textbox', {name: 'Monthly Max'})).toHaveValue('20');
});
it('prefills subscription data based on price with annual plan', async function () {
const sub = SubscriptionFixture({
organization,
plan: 'am1_business_auf',
planTier: 'am1',
categories: {
errors: MetricHistoryFixture({reserved: 100_000}),
transactions: MetricHistoryFixture({reserved: 20_000_000}),
replays: MetricHistoryFixture({reserved: 500}),
attachments: MetricHistoryFixture({reserved: 1}),
monitorSeats: MetricHistoryFixture({reserved: 1}),
},
onDemandMaxSpend: 2000,
});
SubscriptionStore.set(organization.slug, sub);
render(
,
{organization}
);
expect(
await screen.findByRole('heading', {name: 'Change Subscription'})
).toBeInTheDocument();
expect(screen.getByRole('radio', {name: 'Business'})).toBeChecked();
await userEvent.click(screen.getByText('Reserved Volumes'));
// TODO: Can better write this once we have
// https://github.com/testing-library/jest-dom/issues/478
expect(screen.getByRole('slider', {name: 'Errors'})).toHaveAttribute(
'aria-valuetext',
'100000'
);
expect(screen.getByRole('slider', {name: 'Performance units'})).toHaveAttribute(
'aria-valuetext',
'35000000'
);
expect(screen.getByRole('slider', {name: 'Replays'})).toHaveAttribute(
'aria-valuetext',
'500'
);
expect(screen.getByRole('slider', {name: 'Attachments'})).toHaveAttribute(
'aria-valuetext',
'1'
);
await userEvent.click(screen.getByText('On-Demand Max Spend'));
expect(screen.getByRole('textbox', {name: 'Monthly Max'})).toHaveValue('20');
});
it('prefills subscription data based on events with different plan type', async function () {
const sub = SubscriptionFixture({
organization,
plan: 'am1_team',
planTier: 'am1',
categories: {
errors: MetricHistoryFixture({reserved: 100_000}),
transactions: MetricHistoryFixture({reserved: 20_000_000}),
attachments: MetricHistoryFixture({reserved: 1}),
replays: MetricHistoryFixture({reserved: 500}),
monitorSeats: MetricHistoryFixture({reserved: 1}),
},
onDemandMaxSpend: 2000,
});
SubscriptionStore.set(organization.slug, sub);
render(
,
{organization}
);
expect(
await screen.findByRole('heading', {name: 'Change Subscription'})
).toBeInTheDocument();
expect(screen.getByRole('radio', {name: 'Business'})).toBeChecked();
await userEvent.click(screen.getByText('Reserved Volumes'));
// TODO: Can better write this once we have
// https://github.com/testing-library/jest-dom/issues/478
expect(screen.getByRole('slider', {name: 'Errors'})).toHaveAttribute(
'aria-valuetext',
'100000'
);
expect(screen.getByRole('slider', {name: 'Performance units'})).toHaveAttribute(
'aria-valuetext',
'20000000'
);
expect(screen.getByRole('slider', {name: 'Replays'})).toHaveAttribute(
'aria-valuetext',
'500'
);
expect(screen.getByRole('slider', {name: 'Attachments'})).toHaveAttribute(
'aria-valuetext',
'1'
);
await userEvent.click(screen.getByText('On-Demand Max Spend'));
expect(screen.getByRole('textbox', {name: 'Monthly Max'})).toHaveValue('20');
});
it('displays 40% india promotion', async function () {
const promotionData = {
completedPromotions: [
{
promotion: {
name: 'Test Promotion',
slug: 'test_promotion',
timeLimit: null,
startDate: null,
endDate: null,
showDiscountInfo: true,
discountInfo: {
amount: 4000,
billingInterval: 'monthly',
billingPeriods: 3,
creditCategory: 'subscription',
discountType: 'percentPoints',
disclaimerText:
"*Receive 40% off the monthly price of Sentry's Team or Business plan subscriptions for your first three months if you upgrade today",
durationText: 'First three months',
},
},
},
],
};
MockApiClient.addMockResponse({
url: `/organizations/${organization.slug}/promotions/trigger-check/`,
method: 'POST',
body: promotionData,
});
render(
,
{organization}
);
await screen.findByText('Choose Your Plan');
expect(
screen.getByText(
"*Receive 40% off the monthly price of Sentry's Team or Business plan subscriptions for your first three months if you upgrade today"
)
).toBeInTheDocument();
expect(screen.getByText('First three months 40% off')).toBeInTheDocument();
expect(screen.getAllByText('53.40')).toHaveLength(2);
});
it('skips step 1 for business plan in same tier', async function () {
const am2BizSubscription = SubscriptionFixture({
organization,
plan: 'am2_business',
planTier: 'am2',
categories: {
errors: MetricHistoryFixture({reserved: 100_000}),
transactions: MetricHistoryFixture({reserved: 20_000_000}),
attachments: MetricHistoryFixture({reserved: 1}),
monitorSeats: MetricHistoryFixture({reserved: 1}),
profileDuration: MetricHistoryFixture({reserved: 1}),
replays: MetricHistoryFixture({reserved: 10_000}),
},
onDemandMaxSpend: 2000,
});
SubscriptionStore.set(organization.slug, am2BizSubscription);
render(
,
{organization}
);
await screen.findByText('Choose Your Plan');
expect(screen.queryByTestId('body-choose-your-plan')).not.toBeInTheDocument();
expect(screen.getByTestId('errors-volume-item')).toBeInTheDocument();
});
it('test business bundle standard checkout', async function () {
const am2BizSubscription = SubscriptionFixture({
organization,
plan: 'am2_business_bundle',
planTier: 'am2',
categories: {
errors: MetricHistoryFixture({reserved: 100_000}),
transactions: MetricHistoryFixture({reserved: 20_000_000}),
attachments: MetricHistoryFixture({reserved: 1}),
monitorSeats: MetricHistoryFixture({reserved: 1}),
profileDuration: MetricHistoryFixture({reserved: 1}),
replays: MetricHistoryFixture({reserved: 10_000}),
},
onDemandMaxSpend: 2000,
});
SubscriptionStore.set(organization.slug, am2BizSubscription);
render(
,
{organization}
);
// wait for page load
await screen.findByText('Choose Your Plan');
// "Choose Your Plan" should be skipped and "Reserved Volumes" should be visible
// This is existing behavior to skip "Choose Your Plan" step for existing business customers
expect(screen.queryByTestId('body-choose-your-plan')).not.toBeInTheDocument();
expect(screen.getByTestId('errors-volume-item')).toBeInTheDocument();
// Click on "Choose Your Plan" and verify that Business is selected
await userEvent.click(screen.getByText('Choose Your Plan'));
expect(screen.getByRole('radio', {name: 'Business'})).toBeChecked();
});
it('handles missing categories in subscription.categories', async function () {
/**
* In this test, we create a subscription where some categories are missing from
* `subscription.categories`. We then verify that the component renders correctly
* without throwing errors, and that the missing categories default to a reserved
* value of 0.
*/
const sub = SubscriptionFixture({
organization,
plan: 'am2_business',
planTier: 'am2',
categories: {
// Intentionally omitting 'transactions' and 'replays' categories
errors: MetricHistoryFixture({reserved: 100_000}),
attachments: MetricHistoryFixture({reserved: 1}),
monitorSeats: MetricHistoryFixture({reserved: 1}),
},
onDemandMaxSpend: 2000,
});
SubscriptionStore.set(organization.slug, sub);
render(
,
{organization}
);
expect(
await screen.findByRole('heading', {name: 'Change Subscription'})
).toBeInTheDocument();
// Verify that the component renders without errors
expect(screen.getByTestId('errors-volume-item')).toBeInTheDocument();
// Open 'Reserved Volumes' section
await userEvent.click(screen.getByText('Reserved Volumes'));
// Check that missing categories default to 0
expect(screen.getByRole('slider', {name: 'Errors'})).toHaveAttribute(
'aria-valuetext',
'100000'
);
// For missing 'Performance units', should default to 100,000 units
expect(screen.getByRole('slider', {name: 'Performance units'})).toHaveAttribute(
'aria-valuetext',
'100000'
);
// For missing 'Replays', should default to 500
expect(screen.getByRole('slider', {name: 'Replays'})).toHaveAttribute(
'aria-valuetext',
'500'
);
// Check that 'Attachments' category is correctly set
expect(screen.getByRole('slider', {name: 'Attachments'})).toHaveAttribute(
'aria-valuetext',
'1'
);
// Open 'On-Demand Max Spend' section
await userEvent.click(screen.getByText('On-Demand Max Spend'));
expect(screen.getByRole('textbox', {name: 'Monthly Max'})).toHaveValue('20');
});
});
describe('AM3 Checkout', function () {
const api = new MockApiClient();
const organization = OrganizationFixture({
features: ['ondemand-budgets', 'am3-billing'],
});
const params = {};
beforeEach(function () {
MockApiClient.clearMockResponses();
MockApiClient.addMockResponse({
url: `/subscriptions/${organization.slug}/`,
method: 'GET',
body: {},
});
MockApiClient.addMockResponse({
url: `/organizations/${organization.slug}/promotions/trigger-check/`,
method: 'POST',
});
MockApiClient.addMockResponse({
url: `/customers/${organization.slug}/plan-migrations/?applied=0`,
method: 'GET',
body: {},
});
});
it('renders for new customers (AM3 free plan)', async function () {
const sub = SubscriptionFixture({
organization,
plan: 'am3_f',
planTier: PlanTier.AM3,
});
act(() => SubscriptionStore.set(organization.slug, sub));
const mockResponse = MockApiClient.addMockResponse({
url: `/customers/${organization.slug}/billing-config/`,
method: 'GET',
body: BillingConfigFixture(PlanTier.AM3),
});
render(
,
{organization}
);
expect(await screen.findByText('Set Your Pay-as-you-go Budget')).toBeInTheDocument();
expect(mockResponse).toHaveBeenCalledWith(
`/customers/${organization.slug}/billing-config/`,
expect.objectContaining({
method: 'GET',
data: {tier: PlanTier.AM3},
})
);
});
it('renders for customers migrating from partner billing', async function () {
organization.features.push('partner-billing-migration');
const contractPeriodEnd = moment();
const sub = SubscriptionFixture({
organization,
contractPeriodEnd: contractPeriodEnd.toString(),
plan: 'am2_sponsored_team_auf',
planTier: PlanTier.AM2,
isSponsored: true,
partner: {
isActive: true,
externalId: 'yuh',
partnership: {
id: 'FOO',
displayName: 'FOO',
supportNote: '',
},
name: '',
},
});
act(() => SubscriptionStore.set(organization.slug, sub));
const mockResponse = MockApiClient.addMockResponse({
url: `/customers/${organization.slug}/billing-config/`,
method: 'GET',
body: BillingConfigFixture(PlanTier.AM3),
});
render(
,
{organization}
);
expect(await screen.findByText('Set Your Pay-as-you-go Budget')).toBeInTheDocument();
expect(
screen.getByText(
'Your promotional plan with FOO ends on ' + contractPeriodEnd.format('ll') + '.'
)
).toBeInTheDocument();
// 500 replays from sponsored plan becomes 50 on am3
expect(screen.getByText('50')).toBeInTheDocument();
expect(mockResponse).toHaveBeenCalledWith(
`/customers/${organization.slug}/billing-config/`,
expect.objectContaining({
method: 'GET',
data: {tier: PlanTier.AM3},
})
);
});
it('renders for self-serve partners', async function () {
const contractPeriodEnd = moment();
const sub = SubscriptionFixture({
organization,
contractPeriodEnd: contractPeriodEnd.toString(),
plan: 'am3_f',
planTier: PlanTier.AM3,
isSelfServePartner: true,
partner: {
isActive: true,
externalId: 'foo',
partnership: {
id: 'XX',
displayName: 'BAR',
supportNote: '',
},
name: '',
},
});
act(() => SubscriptionStore.set(organization.slug, sub));
const mockResponse = MockApiClient.addMockResponse({
url: `/customers/${organization.slug}/billing-config/`,
method: 'GET',
body: BillingConfigFixture(PlanTier.AM3),
});
render(
,
{organization}
);
expect(await screen.findByText('Set Your Pay-as-you-go Budget')).toBeInTheDocument();
expect(await screen.findByText('Contract Term & Discounts')).toBeInTheDocument();
expect(screen.getByText('Review & Confirm')).toBeInTheDocument();
expect(screen.queryByText('Payment Method')).not.toBeInTheDocument();
expect(screen.queryByText('Billing Details')).not.toBeInTheDocument();
expect(
screen.queryByText(
'Your promotional plan with BAR ends on ' + contractPeriodEnd.format('ll') + '.'
)
).not.toBeInTheDocument();
expect(mockResponse).toHaveBeenCalledWith(
`/customers/${organization.slug}/billing-config/`,
expect.objectContaining({
method: 'GET',
data: {tier: PlanTier.AM3},
})
);
});
it('renders for VC partners', async function () {
organization.features.push('vc-marketplace-active-customer');
const contractPeriodEnd = moment();
const sub = SubscriptionFixture({
organization,
contractPeriodEnd: contractPeriodEnd.toString(),
plan: 'am3_f',
planTier: PlanTier.AM3,
isSelfServePartner: true,
partner: {
isActive: true,
externalId: 'foo',
partnership: {
id: 'XX',
displayName: 'XX',
supportNote: '',
},
name: '',
},
});
act(() => SubscriptionStore.set(organization.slug, sub));
const mockResponse = MockApiClient.addMockResponse({
url: `/customers/${organization.slug}/billing-config/`,
method: 'GET',
body: BillingConfigFixture(PlanTier.AM3),
});
render(
,
{organization}
);
expect(await screen.findByText('Set Your Pay-as-you-go Budget')).toBeInTheDocument();
expect(screen.getByText('Review & Confirm')).toBeInTheDocument();
expect(screen.queryByText('Payment Method')).not.toBeInTheDocument();
expect(screen.queryByText('Billing Details')).not.toBeInTheDocument();
expect(screen.queryByText('Contract Term & Discounts')).not.toBeInTheDocument();
expect(mockResponse).toHaveBeenCalledWith(
`/customers/${organization.slug}/billing-config/`,
expect.objectContaining({
method: 'GET',
data: {tier: PlanTier.AM3},
})
);
});
it('does not render for AM2 customers', async function () {
const sub = SubscriptionFixture({
organization,
plan: 'am2_f',
planTier: PlanTier.AM2,
});
act(() => SubscriptionStore.set(organization.slug, sub));
const mockResponse = MockApiClient.addMockResponse({
url: `/customers/${organization.slug}/billing-config/`,
method: 'GET',
body: BillingConfigFixture(PlanTier.AM2),
});
render(
,
{organization}
);
expect(await screen.findByText('Choose Your Plan')).toBeInTheDocument();
expect(screen.queryByText('Set Your Pay-as-you-go Budget')).not.toBeInTheDocument();
expect(mockResponse).toHaveBeenCalledWith(
`/customers/${organization.slug}/billing-config/`,
expect.objectContaining({
method: 'GET',
data: {tier: PlanTier.AM2},
})
);
});
it('does not render for AM1 customers', async function () {
const sub = SubscriptionFixture({
organization,
plan: 'am1_f',
planTier: PlanTier.AM1,
});
act(() => SubscriptionStore.set(organization.slug, sub));
const mockResponse = MockApiClient.addMockResponse({
url: `/customers/${organization.slug}/billing-config/`,
method: 'GET',
body: BillingConfigFixture(PlanTier.AM1),
});
render(
,
{organization}
);
expect(await screen.findByText('Choose Your Plan')).toBeInTheDocument();
expect(screen.queryByText('Set Your Pay-as-you-go Budget')).not.toBeInTheDocument();
expect(mockResponse).toHaveBeenCalledWith(
`/customers/${organization.slug}/billing-config/`,
expect.objectContaining({
method: 'GET',
data: {tier: PlanTier.AM1},
})
);
});
it('prefills with existing subscription data', async function () {
MockApiClient.addMockResponse({
url: `/customers/${organization.slug}/billing-config/`,
method: 'GET',
body: BillingConfigFixture(PlanTier.AM3),
});
const sub = SubscriptionFixture({
organization,
plan: 'am3_business',
planTier: PlanTier.AM3,
categories: {
errors: MetricHistoryFixture({reserved: 100_000}),
attachments: MetricHistoryFixture({reserved: 25}),
replays: MetricHistoryFixture({reserved: 50}),
monitorSeats: MetricHistoryFixture({reserved: 1}),
spans: MetricHistoryFixture({reserved: 20_000_000}),
profileDuration: MetricHistoryFixture({reserved: 1}),
},
onDemandBudgets: {
onDemandSpendUsed: 0,
sharedMaxBudget: 2000,
budgetMode: OnDemandBudgetMode.SHARED,
enabled: true,
},
onDemandMaxSpend: 2000,
supportsOnDemand: true,
});
SubscriptionStore.set(organization.slug, sub);
render(
,
{organization}
);
expect(
await screen.findByRole('heading', {name: 'Change Subscription'})
).toBeInTheDocument();
expect(screen.getByTestId('errors-volume-item')).toBeInTheDocument(); // skips over first step when subscription is already on Business plan
expect(screen.getByRole('textbox', {name: 'Pay-as-you-go budget'})).toHaveValue('20');
// TODO: Can better write this once we have
// https://github.com/testing-library/jest-dom/issues/478
expect(screen.getByRole('slider', {name: 'Errors'})).toHaveAttribute(
'aria-valuetext',
'100000'
);
expect(screen.getByRole('slider', {name: 'Replays'})).toHaveAttribute(
'aria-valuetext',
'50'
);
expect(screen.getByRole('slider', {name: 'Spans'})).toHaveAttribute(
'aria-valuetext',
'20000000'
);
expect(screen.getByRole('slider', {name: 'Attachments'})).toHaveAttribute(
'aria-valuetext',
'25'
);
expect(
screen.queryByRole('slider', {name: 'Accepted Spans'})
).not.toBeInTheDocument();
expect(screen.queryByRole('slider', {name: 'Stored Spans'})).not.toBeInTheDocument();
expect(screen.queryByRole('slider', {name: 'Cron Monitors'})).not.toBeInTheDocument();
});
it('allows setting PAYG for customers switching to AM3', async function () {
const sub = SubscriptionFixture({
organization,
// This plan does not have hasOnDemandModes
plan: 'mm2_b_100k',
planTier: PlanTier.AM2,
});
act(() => SubscriptionStore.set(organization.slug, sub));
const mockResponse = MockApiClient.addMockResponse({
url: `/customers/${organization.slug}/billing-config/`,
method: 'GET',
body: BillingConfigFixture(PlanTier.AM3),
});
render(
,
{organization}
);
expect(hasOnDemandBudgetsFeature(organization, sub)).toBe(false);
expect(await screen.findByText('Choose Your Plan')).toBeInTheDocument();
expect(screen.getByRole('radio', {name: 'Business'})).toBeChecked();
await userEvent.click(screen.getByRole('button', {name: 'Continue'}));
await userEvent.clear(screen.getByRole('textbox', {name: 'Pay-as-you-go budget'}));
await userEvent.type(
screen.getByRole('textbox', {name: 'Pay-as-you-go budget'}),
'20'
);
expect(await screen.findByTestId('additional-monthly-charge')).toHaveTextContent(
'+ up to $20/mo based on PAYG usage'
);
expect(mockResponse).toHaveBeenCalledWith(
`/customers/${organization.slug}/billing-config/`,
expect.objectContaining({
method: 'GET',
data: {tier: PlanTier.AM3},
})
);
});
it('handles missing categories in subscription.categories', async function () {
// Add billing config mock response
MockApiClient.addMockResponse({
url: `/customers/${organization.slug}/billing-config/`,
method: 'GET',
body: BillingConfigFixture(PlanTier.AM3),
});
/**
* In this test, we create a subscription where some categories are missing from
* `subscription.categories`. We then verify that the component renders correctly
* without throwing errors, and that the missing categories default to a reserved
* value of 0.
*/
const sub = SubscriptionFixture({
organization,
plan: 'am3_business',
planTier: PlanTier.AM3,
categories: {
// Intentionally omitting 'errors' and 'attachments' categories
replays: MetricHistoryFixture({reserved: 50}),
monitorSeats: MetricHistoryFixture({reserved: 1}),
spans: MetricHistoryFixture({reserved: 1}),
profileDuration: MetricHistoryFixture({reserved: 1}),
},
onDemandBudgets: {
onDemandSpendUsed: 0,
sharedMaxBudget: 2000,
budgetMode: OnDemandBudgetMode.SHARED,
enabled: true,
},
supportsOnDemand: true,
});
SubscriptionStore.set(organization.slug, sub);
render(
,
{organization}
);
expect(
await screen.findByRole('heading', {name: 'Change Subscription'})
).toBeInTheDocument();
// Verify that the component renders without errors
expect(screen.getByTestId('replays-volume-item')).toBeInTheDocument();
// For AM3, we should see "Set Your Pay-as-you-go Budget" first
expect(screen.getByText('Set Your Pay-as-you-go Budget')).toBeInTheDocument();
// Check that missing 'Errors' category defaults to 50,000 errors
expect(screen.getByRole('slider', {name: 'Errors'})).toHaveAttribute(
'aria-valuetext',
'50000'
);
// For 'Replays', should be set to 50 as per the subscription
expect(screen.getByRole('slider', {name: 'Replays'})).toHaveAttribute(
'aria-valuetext',
'50'
);
// Check that missing 'Attachments' category defaults to 1 GB
expect(screen.getByRole('slider', {name: 'Attachments'})).toHaveAttribute(
'aria-valuetext',
'1'
);
// Verify that the 'Pay-as-you-go budget' is correctly set
expect(screen.getByRole('textbox', {name: 'Pay-as-you-go budget'})).toHaveValue('20');
});
it('handles zero platform reserve', function () {
const formData = {
plan: 'am3_business',
reserved: {
errors: 10000,
transactions: 0,
attachments: 0,
replays: 0,
monitorSeats: 0,
profileDuration: 0,
spans: 0,
},
};
expect(getCheckoutAPIData({formData})).toEqual({
onDemandBudget: undefined,
onDemandMaxSpend: 0,
plan: 'am3_business',
referrer: 'billing',
reservedErrors: 10000,
reservedTransactions: 0,
reservedAttachments: 0,
reservedReplays: 0,
reservedMonitorSeats: 0,
reservedProfileDuration: 0,
reservedSpans: 0,
});
});
});