planSelect.spec.tsx 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527
  1. import moment from 'moment-timezone';
  2. import {LocationFixture} from 'sentry-fixture/locationFixture';
  3. import {OrganizationFixture} from 'sentry-fixture/organization';
  4. import {RouteComponentPropsFixture} from 'sentry-fixture/routeComponentPropsFixture';
  5. import {BillingConfigFixture} from 'getsentry-test/fixtures/billingConfig';
  6. import {SubscriptionFixture} from 'getsentry-test/fixtures/subscription';
  7. import {render, screen, userEvent, within} from 'sentry-test/reactTestingLibrary';
  8. import {textWithMarkupMatcher} from 'sentry-test/utils';
  9. import {ANNUAL} from 'getsentry/constants';
  10. import SubscriptionStore from 'getsentry/stores/subscriptionStore';
  11. import {PlanTier} from 'getsentry/types';
  12. import AMCheckout from 'getsentry/views/amCheckout/';
  13. describe('PlanSelect', function () {
  14. const api = new MockApiClient();
  15. const organization = OrganizationFixture();
  16. const subscription = SubscriptionFixture({organization});
  17. const params = {};
  18. beforeEach(function () {
  19. SubscriptionStore.set(organization.slug, subscription);
  20. MockApiClient.addMockResponse({
  21. url: `/subscriptions/${organization.slug}/`,
  22. method: 'GET',
  23. body: {},
  24. });
  25. MockApiClient.addMockResponse({
  26. url: `/customers/${organization.slug}/billing-config/`,
  27. method: 'GET',
  28. body: BillingConfigFixture(PlanTier.AM2),
  29. });
  30. MockApiClient.addMockResponse({
  31. method: 'POST',
  32. url: '/_experiment/log_exposure/',
  33. body: {},
  34. });
  35. MockApiClient.addMockResponse({
  36. url: `/organizations/${organization.slug}/promotions/trigger-check/`,
  37. method: 'POST',
  38. body: {},
  39. });
  40. MockApiClient.addMockResponse({
  41. url: `/customers/${organization.slug}/plan-migrations/?applied=0`,
  42. method: 'GET',
  43. body: {},
  44. });
  45. });
  46. it('renders', async function () {
  47. const freeSubscription = SubscriptionFixture({
  48. organization,
  49. plan: 'am2_f',
  50. isFree: true,
  51. });
  52. SubscriptionStore.set(organization.slug, freeSubscription);
  53. render(
  54. <AMCheckout
  55. {...RouteComponentPropsFixture()}
  56. params={params}
  57. api={api}
  58. onToggleLegacy={jest.fn()}
  59. checkoutTier={PlanTier.AM2}
  60. />,
  61. {organization}
  62. );
  63. expect(await screen.findByTestId('body-choose-your-plan')).toBeInTheDocument();
  64. expect(screen.getByTestId('footer-choose-your-plan')).toBeInTheDocument();
  65. });
  66. it('renders checkmarks on team plan', async function () {
  67. const org = OrganizationFixture();
  68. const teamSubscription = SubscriptionFixture({
  69. organization: org,
  70. plan: 'am2_team',
  71. isFree: false,
  72. });
  73. SubscriptionStore.set(org.slug, teamSubscription);
  74. render(
  75. <AMCheckout
  76. {...RouteComponentPropsFixture()}
  77. params={params}
  78. api={api}
  79. onToggleLegacy={jest.fn()}
  80. checkoutTier={PlanTier.AM2}
  81. />,
  82. {organization: org}
  83. );
  84. const teamPlan = await screen.findByLabelText('Team');
  85. const businessPlan = screen.getByLabelText('Business');
  86. expect(teamPlan).toHaveTextContent('Current plan');
  87. expect(within(teamPlan).getAllByTestId('icon-check-mark')).toHaveLength(3);
  88. expect(within(businessPlan).queryByTestId('icon-check-mark')).not.toBeInTheDocument();
  89. });
  90. it('marks business as the current plan', async function () {
  91. const org = OrganizationFixture();
  92. const businessSubscription = SubscriptionFixture({
  93. organization: org,
  94. plan: 'am2_business',
  95. isFree: false,
  96. });
  97. SubscriptionStore.set(org.slug, businessSubscription);
  98. render(
  99. <AMCheckout
  100. {...RouteComponentPropsFixture()}
  101. params={params}
  102. api={api}
  103. onToggleLegacy={jest.fn()}
  104. checkoutTier={PlanTier.AM2}
  105. />,
  106. {organization: org}
  107. );
  108. const businessPlan = await screen.findByLabelText('Business');
  109. expect(businessPlan).toHaveTextContent('Current plan');
  110. expect(within(businessPlan).queryByTestId('icon-check-mark')).not.toBeInTheDocument();
  111. });
  112. it('renders targeted features when have referrer', async () => {
  113. const org = OrganizationFixture();
  114. const sub = SubscriptionFixture({
  115. organization: org,
  116. plan: 'am2_team',
  117. isFree: false,
  118. });
  119. SubscriptionStore.set(org.slug, sub);
  120. render(
  121. <AMCheckout
  122. {...RouteComponentPropsFixture()}
  123. params={params}
  124. api={api}
  125. onToggleLegacy={jest.fn()}
  126. checkoutTier={PlanTier.AM2}
  127. location={LocationFixture({
  128. query: {
  129. referrer: 'upgrade-business-landing.relay',
  130. },
  131. })}
  132. />,
  133. {organization: org}
  134. );
  135. const businessPlan = await screen.findByLabelText('Business');
  136. const advancedFiltering = within(businessPlan)
  137. .getByText('Advanced server-side filtering')
  138. .closest('div');
  139. if (advancedFiltering) {
  140. expect(
  141. within(advancedFiltering).getByText('Looking for this?')
  142. ).toBeInTheDocument();
  143. }
  144. const warningText = textWithMarkupMatcher(
  145. 'This plan does not include Advanced server-side filtering'
  146. );
  147. expect(screen.queryByText(warningText)).not.toBeInTheDocument();
  148. // Clicking the team plan shows a warning that the plan doesn't include the
  149. // feature they want
  150. await userEvent.click(screen.getByLabelText('Team'));
  151. expect(screen.getByText(warningText)).toBeInTheDocument();
  152. });
  153. it('renders with correct default prices', async function () {
  154. render(
  155. <AMCheckout
  156. {...RouteComponentPropsFixture()}
  157. params={params}
  158. api={api}
  159. onToggleLegacy={jest.fn()}
  160. checkoutTier={PlanTier.AM2}
  161. />,
  162. {organization}
  163. );
  164. const teamPlan = await screen.findByLabelText('Team');
  165. const businessPlan = screen.getByLabelText('Business');
  166. expect(teamPlan).toHaveTextContent('$29/mo');
  167. expect(businessPlan).toHaveTextContent('$89/mo');
  168. });
  169. it('renders with default plan selected', async function () {
  170. render(
  171. <AMCheckout
  172. {...RouteComponentPropsFixture()}
  173. params={params}
  174. api={api}
  175. onToggleLegacy={jest.fn()}
  176. checkoutTier={PlanTier.AM2}
  177. />,
  178. {organization}
  179. );
  180. const teamPlan = await screen.findByLabelText('Team');
  181. const businessPlan = screen.getByLabelText('Business');
  182. expect(within(teamPlan).getByRole('radio')).not.toBeChecked();
  183. expect(within(businessPlan).getByRole('radio')).toBeChecked();
  184. });
  185. it('can select plan', async function () {
  186. render(
  187. <AMCheckout
  188. {...RouteComponentPropsFixture()}
  189. params={params}
  190. api={api}
  191. onToggleLegacy={jest.fn()}
  192. checkoutTier={PlanTier.AM2}
  193. />,
  194. {organization}
  195. );
  196. const teamPlan = await screen.findByLabelText('Team');
  197. const businessPlan = screen.getByLabelText('Business');
  198. expect(within(teamPlan).getByRole('radio')).not.toBeChecked();
  199. expect(within(businessPlan).getByRole('radio')).toBeChecked();
  200. await userEvent.click(teamPlan);
  201. expect(within(teamPlan).getByRole('radio')).toBeChecked();
  202. expect(within(businessPlan).getByRole('radio')).not.toBeChecked();
  203. });
  204. it('can continue', async function () {
  205. render(
  206. <AMCheckout
  207. {...RouteComponentPropsFixture()}
  208. params={params}
  209. api={api}
  210. onToggleLegacy={jest.fn()}
  211. checkoutTier={PlanTier.AM2}
  212. />,
  213. {organization}
  214. );
  215. expect(await screen.findByTestId('body-choose-your-plan')).toBeInTheDocument();
  216. await userEvent.click(screen.getByRole('button', {name: 'Continue'}));
  217. expect(screen.queryByTestId('body-choose-your-plan')).not.toBeInTheDocument();
  218. });
  219. it('can edit', async function () {
  220. render(
  221. <AMCheckout
  222. {...RouteComponentPropsFixture()}
  223. params={params}
  224. api={api}
  225. onToggleLegacy={jest.fn()}
  226. checkoutTier={PlanTier.AM2}
  227. />,
  228. {organization}
  229. );
  230. expect(await screen.findByTestId('body-choose-your-plan')).toBeInTheDocument();
  231. await userEvent.click(screen.getByRole('button', {name: 'Continue'}));
  232. expect(screen.queryByTestId('body-choose-your-plan')).not.toBeInTheDocument();
  233. // Clicking the header opens it back up for editing
  234. await userEvent.click(screen.getByTestId('header-choose-your-plan'));
  235. expect(screen.getByTestId('body-choose-your-plan')).toBeInTheDocument();
  236. });
  237. it('selects business for am2 monthly plans', async function () {
  238. const teamOrganization = OrganizationFixture();
  239. const teamSubscription = SubscriptionFixture({
  240. organization: teamOrganization,
  241. plan: 'am2_team',
  242. isFree: false,
  243. });
  244. SubscriptionStore.set(organization.slug, teamSubscription);
  245. render(
  246. <AMCheckout
  247. {...RouteComponentPropsFixture()}
  248. params={params}
  249. api={api}
  250. onToggleLegacy={jest.fn()}
  251. checkoutTier={PlanTier.AM2}
  252. location={LocationFixture({
  253. query: {
  254. referrer: 'upsell-test',
  255. },
  256. })}
  257. />,
  258. {organization}
  259. );
  260. const teamPlan = await screen.findByLabelText('Team');
  261. const businessPlan = screen.getByLabelText('Business');
  262. expect(within(teamPlan).getByRole('radio')).not.toBeChecked();
  263. expect(within(businessPlan).getByRole('radio')).toBeChecked();
  264. expect(teamPlan).toHaveTextContent('Current plan');
  265. });
  266. it('selects business for am2 annual plans', async function () {
  267. const teamOrganization = OrganizationFixture();
  268. const teamSubscription = SubscriptionFixture({
  269. organization: teamOrganization,
  270. plan: 'am2_team_auf',
  271. contractInterval: ANNUAL,
  272. isFree: false,
  273. });
  274. SubscriptionStore.set(organization.slug, teamSubscription);
  275. render(
  276. <AMCheckout
  277. {...RouteComponentPropsFixture()}
  278. params={params}
  279. api={api}
  280. onToggleLegacy={jest.fn()}
  281. checkoutTier={PlanTier.AM2}
  282. location={LocationFixture({
  283. query: {
  284. referrer: 'upsell-test',
  285. },
  286. })}
  287. />,
  288. {organization}
  289. );
  290. const teamPlan = await screen.findByLabelText('Team');
  291. const businessPlan = screen.getByLabelText('Business');
  292. expect(within(teamPlan).getByRole('radio')).not.toBeChecked();
  293. expect(within(businessPlan).getByRole('radio')).toBeChecked();
  294. expect(teamPlan).toHaveTextContent('Current plan');
  295. });
  296. it('selects team for non upsell referrers', async function () {
  297. const teamOrganization = OrganizationFixture();
  298. const teamSubscription = SubscriptionFixture({
  299. organization: teamOrganization,
  300. plan: 'am2_team',
  301. isFree: false,
  302. });
  303. SubscriptionStore.set(organization.slug, teamSubscription);
  304. render(
  305. <AMCheckout
  306. {...RouteComponentPropsFixture()}
  307. params={params}
  308. api={api}
  309. onToggleLegacy={jest.fn()}
  310. checkoutTier={PlanTier.AM2}
  311. location={LocationFixture({
  312. query: {
  313. referrer: 'random_referrer',
  314. },
  315. })}
  316. />,
  317. {organization}
  318. );
  319. const teamPlan = await screen.findByLabelText('Team');
  320. const businessPlan = screen.getByLabelText('Business');
  321. expect(within(businessPlan).getByRole('radio')).not.toBeChecked();
  322. expect(within(teamPlan).getByRole('radio')).toBeChecked();
  323. expect(teamPlan).toHaveTextContent('Current plan');
  324. });
  325. it('shows plan hint', async function () {
  326. const teamOrganization = OrganizationFixture();
  327. const teamSubscription = SubscriptionFixture({
  328. organization: teamOrganization,
  329. plan: 'am2_team',
  330. lastTrialEnd: '2000/05/01',
  331. isFree: false,
  332. });
  333. SubscriptionStore.set(organization.slug, teamSubscription);
  334. render(
  335. <AMCheckout
  336. {...RouteComponentPropsFixture()}
  337. params={params}
  338. api={api}
  339. onToggleLegacy={jest.fn()}
  340. checkoutTier={PlanTier.AM2}
  341. location={LocationFixture({
  342. query: {
  343. referrer: 'random_referrer',
  344. },
  345. })}
  346. />,
  347. {organization: teamOrganization}
  348. );
  349. const teamPlan = await screen.findByLabelText('Team');
  350. const businessPlan = screen.getByLabelText('Business');
  351. expect(within(businessPlan).getByRole('radio')).not.toBeChecked();
  352. expect(within(teamPlan).getByRole('radio')).toBeChecked();
  353. expect(businessPlan).toHaveTextContent('You trialed this plan');
  354. });
  355. it('shows trial expires', async function () {
  356. const teamOrganization = OrganizationFixture();
  357. const teamSubscription = SubscriptionFixture({
  358. organization: teamOrganization,
  359. plan: 'am2_t',
  360. lastTrialEnd: moment().utc().add(2, 'days').format(),
  361. isFree: false,
  362. isTrial: true,
  363. });
  364. SubscriptionStore.set(organization.slug, teamSubscription);
  365. render(
  366. <AMCheckout
  367. {...RouteComponentPropsFixture()}
  368. params={params}
  369. api={api}
  370. onToggleLegacy={jest.fn()}
  371. checkoutTier={PlanTier.AM2}
  372. location={LocationFixture({
  373. query: {
  374. referrer: 'random_referrer',
  375. },
  376. })}
  377. />,
  378. {organization: teamOrganization}
  379. );
  380. await screen.findByLabelText('Business');
  381. const businessPlan = screen.getByLabelText('Business');
  382. expect(businessPlan).toHaveTextContent('Trial expires in 2 days');
  383. });
  384. it('shows plan trialed', async function () {
  385. const teamOrganization = OrganizationFixture();
  386. const teamSubscription = SubscriptionFixture({
  387. organization: teamOrganization,
  388. plan: 'am2_t',
  389. lastTrialEnd: moment().utc().subtract(1, 'days').format(),
  390. isFree: false,
  391. isTrial: true,
  392. });
  393. SubscriptionStore.set(organization.slug, teamSubscription);
  394. render(
  395. <AMCheckout
  396. {...RouteComponentPropsFixture()}
  397. params={params}
  398. api={api}
  399. onToggleLegacy={jest.fn()}
  400. checkoutTier={PlanTier.AM2}
  401. location={LocationFixture({
  402. query: {
  403. referrer: 'random_referrer',
  404. },
  405. })}
  406. />,
  407. {organization: teamOrganization}
  408. );
  409. await screen.findByLabelText('Business');
  410. const businessPlan = screen.getByLabelText('Business');
  411. expect(businessPlan).toHaveTextContent('You trialed this plan');
  412. });
  413. it('calls prompts activity when business to team downgrade', async function () {
  414. const mockPromptUpdate = MockApiClient.addMockResponse({
  415. url: `/organizations/${organization.slug}/prompts-activity/`,
  416. method: 'PUT',
  417. body: {
  418. organizationId: organization.id,
  419. feature: 'business_to_team_promo',
  420. status: 'dismissed',
  421. },
  422. });
  423. const org = OrganizationFixture();
  424. const businessSubscription = SubscriptionFixture({
  425. organization: org,
  426. plan: 'am2_business',
  427. isFree: false,
  428. });
  429. SubscriptionStore.set(org.slug, businessSubscription);
  430. render(
  431. <AMCheckout
  432. {...RouteComponentPropsFixture()}
  433. params={params}
  434. api={api}
  435. onToggleLegacy={jest.fn()}
  436. checkoutTier={PlanTier.AM2}
  437. />,
  438. {organization}
  439. );
  440. const teamPlan = await screen.findByLabelText('Team');
  441. await userEvent.click(teamPlan);
  442. await userEvent.click(screen.getByRole('button', {name: 'Continue'}));
  443. expect(mockPromptUpdate).toHaveBeenCalled();
  444. });
  445. });