From 70013735cc42a4ab2bdd2d61bde727f41fa2687a Mon Sep 17 00:00:00 2001 From: Rishabh Date: Thu, 17 Feb 2022 11:30:10 +0530 Subject: [PATCH] Added tests for free member upgrade flows closes https://github.com/TryGhost/Team/issues/1348 closes https://github.com/TryGhost/Team/issues/1352 - also updates dev mode check to use dummy member for offers page --- ghost/portal/src/App.js | 4 +- ghost/portal/src/pages.js | 4 + ghost/portal/src/tests/UpgradeFlow.test.js | 360 +++++++++++++++++++++ ghost/portal/src/utils/test-fixtures.js | 9 + 4 files changed, 375 insertions(+), 2 deletions(-) create mode 100644 ghost/portal/src/tests/UpgradeFlow.test.js diff --git a/ghost/portal/src/App.js b/ghost/portal/src/App.js index ae37462c66..a9b11e0b75 100644 --- a/ghost/portal/src/App.js +++ b/ghost/portal/src/App.js @@ -5,8 +5,8 @@ import PopupModal from './components/PopupModal'; import setupGhostApi from './utils/api'; import AppContext from './AppContext'; import {hasMode} from './utils/check-mode'; -import {getActivePage, isAccountPage} from './pages'; import * as Fixtures from './utils/fixtures'; +import {getActivePage, isAccountPage, isOfferPage} from './pages'; import ActionHandler from './actions'; import './App.css'; import NotificationParser from './utils/notifications'; @@ -728,7 +728,7 @@ export default class App extends React.Component { getContextMember({page, member, customSiteUrl}) { if (hasMode(['dev', 'preview'], {customSiteUrl})) { /** Use dummy member(free or paid) for account pages in dev/preview mode*/ - if (isAccountPage({page})) { + if (isAccountPage({page}) || isOfferPage({page})) { if (hasMode(['dev'], {customSiteUrl})) { return member || Fixtures.member.free; } else if (hasMode(['preview'])) { diff --git a/ghost/portal/src/pages.js b/ghost/portal/src/pages.js index ac86421d39..2718bde9f9 100644 --- a/ghost/portal/src/pages.js +++ b/ghost/portal/src/pages.js @@ -33,4 +33,8 @@ export const isAccountPage = function ({page}) { return page.includes('account'); }; +export const isOfferPage = function ({page}) { + return page.includes('offer'); +}; + export default Pages; diff --git a/ghost/portal/src/tests/UpgradeFlow.test.js b/ghost/portal/src/tests/UpgradeFlow.test.js new file mode 100644 index 0000000000..f758c2fd32 --- /dev/null +++ b/ghost/portal/src/tests/UpgradeFlow.test.js @@ -0,0 +1,360 @@ +import React from 'react'; +import App from '../App.js'; +import {fireEvent, appRender, within} from '../utils/test-utils'; +import {offer as FixtureOffer, site as FixtureSite, member as FixtureMember} from '../utils/test-fixtures'; +import setupGhostApi from '../utils/api.js'; + +const offerSetup = async ({site, member = null, offer}) => { + const ghostApi = setupGhostApi({siteUrl: 'https://example.com'}); + ghostApi.init = jest.fn(() => { + return Promise.resolve({ + site, + member + }); + }); + + ghostApi.member.sendMagicLink = jest.fn(() => { + return Promise.resolve('success'); + }); + + ghostApi.site.offer = jest.fn(() => { + return Promise.resolve({ + offers: [offer] + }); + }); + + ghostApi.member.checkoutPlan = jest.fn(() => { + return Promise.resolve(); + }); + + const utils = appRender( + + ); + + const triggerButtonFrame = await utils.findByTitle(/portal-trigger/i); + const popupFrame = utils.queryByTitle(/portal-popup/i); + const popupIframeDocument = popupFrame.contentDocument; + const emailInput = within(popupIframeDocument).queryByLabelText(/email/i); + const nameInput = within(popupIframeDocument).queryByLabelText(/name/i); + const submitButton = within(popupIframeDocument).queryByRole('button', {name: 'Continue'}); + const signinButton = within(popupIframeDocument).queryByRole('button', {name: 'Sign in'}); + const siteTitle = within(popupIframeDocument).queryByText(site.title); + const offerName = within(popupIframeDocument).queryByText(offer.name); + const offerDescription = within(popupIframeDocument).queryByText(offer.display_description); + + const freePlanTitle = within(popupIframeDocument).queryByText('Free'); + const monthlyPlanTitle = within(popupIframeDocument).queryByText('Monthly'); + const yearlyPlanTitle = within(popupIframeDocument).queryByText('Yearly'); + const fullAccessTitle = within(popupIframeDocument).queryByText('Full access'); + return { + ghostApi, + popupIframeDocument, + popupFrame, + triggerButtonFrame, + siteTitle, + emailInput, + nameInput, + signinButton, + submitButton, + freePlanTitle, + monthlyPlanTitle, + yearlyPlanTitle, + fullAccessTitle, + offerName, + offerDescription, + ...utils + }; +}; + +const setup = async ({site, member = null}) => { + const ghostApi = setupGhostApi({siteUrl: 'https://example.com'}); + ghostApi.init = jest.fn(() => { + return Promise.resolve({ + site, + member + }); + }); + + ghostApi.member.sendMagicLink = jest.fn(() => { + return Promise.resolve('success'); + }); + + ghostApi.member.checkoutPlan = jest.fn(() => { + return Promise.resolve(); + }); + + const utils = appRender( + + ); + + const triggerButtonFrame = await utils.findByTitle(/portal-trigger/i); + const popupFrame = utils.queryByTitle(/portal-popup/i); + const popupIframeDocument = popupFrame.contentDocument; + const emailInput = within(popupIframeDocument).queryByLabelText(/email/i); + const nameInput = within(popupIframeDocument).queryByLabelText(/name/i); + const submitButton = within(popupIframeDocument).queryByRole('button', {name: 'Continue'}); + const signinButton = within(popupIframeDocument).queryByRole('button', {name: 'Sign in'}); + const siteTitle = within(popupIframeDocument).queryByText(site.title); + const freePlanTitle = within(popupIframeDocument).queryByText('Free'); + const monthlyPlanTitle = within(popupIframeDocument).queryByText('Monthly'); + const yearlyPlanTitle = within(popupIframeDocument).queryByText('Yearly'); + const fullAccessTitle = within(popupIframeDocument).queryByText('Full access'); + const accountHomeTitle = within(popupIframeDocument).queryByText('Your account'); + const viewPlansButton = within(popupIframeDocument).queryByRole('button', {name: 'View plans'}); + return { + ghostApi, + popupIframeDocument, + popupFrame, + triggerButtonFrame, + siteTitle, + emailInput, + nameInput, + signinButton, + submitButton, + freePlanTitle, + monthlyPlanTitle, + yearlyPlanTitle, + fullAccessTitle, + accountHomeTitle, + viewPlansButton, + ...utils + }; +}; + +const multiTierSetup = async ({site, member = null}) => { + const ghostApi = setupGhostApi({siteUrl: 'https://example.com'}); + ghostApi.init = jest.fn(() => { + return Promise.resolve({ + site, + member + }); + }); + + ghostApi.member.sendMagicLink = jest.fn(() => { + return Promise.resolve('success'); + }); + + ghostApi.member.checkoutPlan = jest.fn(() => { + return Promise.resolve(); + }); + + const utils = appRender( + + ); + const freeTierDescription = site.products?.find(p => p.type === 'free')?.description; + const triggerButtonFrame = await utils.findByTitle(/portal-trigger/i); + const popupFrame = utils.queryByTitle(/portal-popup/i); + const popupIframeDocument = popupFrame.contentDocument; + const emailInput = within(popupIframeDocument).queryByLabelText(/email/i); + const nameInput = within(popupIframeDocument).queryByLabelText(/name/i); + const submitButton = within(popupIframeDocument).queryByRole('button', {name: 'Continue'}); + const signinButton = within(popupIframeDocument).queryByRole('button', {name: 'Sign in'}); + const siteTitle = within(popupIframeDocument).queryByText(site.title); + const freePlanTitle = within(popupIframeDocument).queryAllByText(/free$/i); + const freePlanDescription = within(popupIframeDocument).queryAllByText(freeTierDescription); + const monthlyPlanTitle = within(popupIframeDocument).queryByText('Monthly'); + const yearlyPlanTitle = within(popupIframeDocument).queryByText('Yearly'); + const fullAccessTitle = within(popupIframeDocument).queryByText('Full access'); + const accountHomeTitle = within(popupIframeDocument).queryByText('Your account'); + const viewPlansButton = within(popupIframeDocument).queryByRole('button', {name: 'View plans'}); + return { + ghostApi, + popupIframeDocument, + popupFrame, + triggerButtonFrame, + siteTitle, + emailInput, + nameInput, + signinButton, + submitButton, + freePlanTitle, + monthlyPlanTitle, + yearlyPlanTitle, + fullAccessTitle, + freePlanDescription, + accountHomeTitle, + viewPlansButton, + ...utils + }; +}; + +describe('Logged-in free member', () => { + describe('can upgrade on single tier site', () => { + test('with default settings on monthly plan', async () => { + const { + ghostApi, popupFrame, triggerButtonFrame, + popupIframeDocument, accountHomeTitle, viewPlansButton + } = await setup({ + site: FixtureSite.singleTier.basic, + member: FixtureMember.free + }); + + expect(popupFrame).toBeInTheDocument(); + expect(triggerButtonFrame).toBeInTheDocument(); + expect(accountHomeTitle).toBeInTheDocument(); + expect(viewPlansButton).toBeInTheDocument(); + + const singleTierProduct = FixtureSite.singleTier.basic.products.find(p => p.type === 'paid'); + + fireEvent.click(viewPlansButton); + await within(popupIframeDocument).findByText('Monthly'); + const submitButton = within(popupIframeDocument).queryByRole('button', {name: 'Continue'}); + + fireEvent.click(submitButton); + expect(ghostApi.member.checkoutPlan).toHaveBeenLastCalledWith({ + metadata: { + checkoutType: 'upgrade' + }, + offerId: undefined, + plan: singleTierProduct.monthlyPrice.id + }); + }); + + test('with default settings on yearly plan', async () => { + const { + ghostApi, popupFrame, triggerButtonFrame, + popupIframeDocument, accountHomeTitle, viewPlansButton + } = await setup({ + site: FixtureSite.singleTier.basic, + member: FixtureMember.free + }); + + expect(popupFrame).toBeInTheDocument(); + expect(triggerButtonFrame).toBeInTheDocument(); + expect(accountHomeTitle).toBeInTheDocument(); + expect(viewPlansButton).toBeInTheDocument(); + + const singleTierProduct = FixtureSite.singleTier.basic.products.find(p => p.type === 'paid'); + + fireEvent.click(viewPlansButton); + await within(popupIframeDocument).findByText('Monthly'); + const yearlyPlanContainer = await within(popupIframeDocument).findByText('Yearly'); + fireEvent.click(yearlyPlanContainer.parentNode); + // added fake timeout for react state delay in setting plan + await new Promise(r => setTimeout(r, 10)); + + const submitButton = within(popupIframeDocument).queryByRole('button', {name: 'Continue'}); + + fireEvent.click(submitButton); + expect(ghostApi.member.checkoutPlan).toHaveBeenLastCalledWith({ + metadata: { + checkoutType: 'upgrade' + }, + offerId: undefined, + plan: singleTierProduct.yearlyPrice.id + }); + }); + + test('to an offer via link', async () => { + window.location.hash = '#/portal/offers/61fa22bd0cbecc7d423d20b3'; + const { + ghostApi, popupFrame, triggerButtonFrame, emailInput, nameInput, signinButton, submitButton, + siteTitle, + offerName, offerDescription + } = await offerSetup({ + site: FixtureSite.singleTier.basic, + member: FixtureMember.altFree, + offer: FixtureOffer + }); + let planId = FixtureSite.singleTier.basic.products.find(p => p.type === 'paid').monthlyPrice.id; + let offerId = FixtureOffer.id; + expect(popupFrame).toBeInTheDocument(); + expect(triggerButtonFrame).toBeInTheDocument(); + expect(siteTitle).toBeInTheDocument(); + expect(emailInput).toBeInTheDocument(); + expect(nameInput).toBeInTheDocument(); + expect(signinButton).not.toBeInTheDocument(); + expect(submitButton).toBeInTheDocument(); + expect(offerName).toBeInTheDocument(); + expect(offerDescription).toBeInTheDocument(); + + expect(emailInput).toHaveValue('jimmie@example.com'); + expect(nameInput).toHaveValue('Jimmie Larson'); + fireEvent.click(submitButton); + + expect(ghostApi.member.checkoutPlan).toHaveBeenLastCalledWith({ + email: 'jimmie@example.com', + name: 'Jimmie Larson', + offerId, + plan: planId + }); + + window.location.hash = ''; + }); + }); +}); + +describe('Logged-in free member', () => { + describe('can upgrade on multi tier site', () => { + test('with default settings', async () => { + const { + ghostApi, popupFrame, triggerButtonFrame, + popupIframeDocument, accountHomeTitle, viewPlansButton + } = await multiTierSetup({ + site: FixtureSite.multipleTiers.basic, + member: FixtureMember.free + }); + + expect(popupFrame).toBeInTheDocument(); + expect(triggerButtonFrame).toBeInTheDocument(); + expect(accountHomeTitle).toBeInTheDocument(); + expect(viewPlansButton).toBeInTheDocument(); + + const singleTierProduct = FixtureSite.multipleTiers.basic.products.find(p => p.type === 'paid'); + + fireEvent.click(viewPlansButton); + await within(popupIframeDocument).findByText('Monthly'); + + // allow default checkbox switch to yearly + await new Promise(r => setTimeout(r, 10)); + + const submitButton = within(popupIframeDocument).queryByRole('button', {name: 'Continue'}); + + fireEvent.click(submitButton); + expect(ghostApi.member.checkoutPlan).toHaveBeenLastCalledWith({ + metadata: { + checkoutType: 'upgrade' + }, + offerId: undefined, + plan: singleTierProduct.yearlyPrice.id + }); + }); + + test('to an offer via link', async () => { + window.location.hash = '#/portal/offers/61fa22bd0cbecc7d423d20b3'; + const { + ghostApi, popupFrame, triggerButtonFrame, emailInput, nameInput, signinButton, submitButton, + siteTitle, + offerName, offerDescription + } = await offerSetup({ + site: FixtureSite.multipleTiers.basic, + member: FixtureMember.altFree, + offer: FixtureOffer + }); + let planId = FixtureSite.multipleTiers.basic.products.find(p => p.type === 'paid').monthlyPrice.id; + let offerId = FixtureOffer.id; + expect(popupFrame).toBeInTheDocument(); + expect(triggerButtonFrame).toBeInTheDocument(); + expect(siteTitle).toBeInTheDocument(); + expect(emailInput).toBeInTheDocument(); + expect(nameInput).toBeInTheDocument(); + expect(signinButton).not.toBeInTheDocument(); + expect(submitButton).toBeInTheDocument(); + expect(offerName).toBeInTheDocument(); + expect(offerDescription).toBeInTheDocument(); + + expect(emailInput).toHaveValue('jimmie@example.com'); + expect(nameInput).toHaveValue('Jimmie Larson'); + fireEvent.click(submitButton); + + expect(ghostApi.member.checkoutPlan).toHaveBeenLastCalledWith({ + email: 'jimmie@example.com', + name: 'Jimmie Larson', + offerId, + plan: planId + }); + + window.location.hash = ''; + }); + }); +}); diff --git a/ghost/portal/src/utils/test-fixtures.js b/ghost/portal/src/utils/test-fixtures.js index d383715a77..e122dc5517 100644 --- a/ghost/portal/src/utils/test-fixtures.js +++ b/ghost/portal/src/utils/test-fixtures.js @@ -153,6 +153,15 @@ export const member = { avatarImage: '', subscribed: true }), + altFree: getMemberData({ + name: 'Jimmie Larson', + email: 'jimmie@example.com', + firstname: 'Jimmie', + subscriptions: [], + paid: false, + avatarImage: '', + subscribed: true + }), paid: getMemberData({ paid: true, subscriptions: [