diff --git a/ghost/portal/src/tests/SignupFlow.test.js b/ghost/portal/src/tests/SignupFlow.test.js index 703d358c66..0b4686bbbc 100644 --- a/ghost/portal/src/tests/SignupFlow.test.js +++ b/ghost/portal/src/tests/SignupFlow.test.js @@ -1,9 +1,71 @@ import React from 'react'; import App from '../App.js'; -import {fireEvent, appRender, within} from '../utils/test-utils'; -import {site as FixtureSite} from '../utils/test-fixtures'; +import {fireEvent, appRender, within, waitFor} from '../utils/test-utils'; +import {offer as FixtureOffer, site as FixtureSite} 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(() => { @@ -17,6 +79,10 @@ const setup = async ({site, member = null}) => { return Promise.resolve('success'); }); + ghostApi.member.checkoutPlan = jest.fn(() => { + return Promise.resolve(); + }); + const utils = appRender( ); @@ -64,6 +130,10 @@ const multiTierSetup = async ({site, member = null}) => { return Promise.resolve('success'); }); + ghostApi.member.checkoutPlan = jest.fn(() => { + return Promise.resolve(); + }); + const utils = appRender( ); @@ -100,9 +170,9 @@ const multiTierSetup = async ({site, member = null}) => { }; }; -describe('Single tier site', () => { - describe('Signup page', () => { - test('renders with default settings', async () => { +describe('Signup', () => { + describe('as free member on single tier site', () => { + test('with default settings', async () => { const { ghostApi, popupFrame, triggerButtonFrame, emailInput, nameInput, signinButton, submitButton, siteTitle, popupIframeDocument, freePlanTitle, monthlyPlanTitle, yearlyPlanTitle, fullAccessTitle @@ -137,7 +207,7 @@ describe('Single tier site', () => { expect(magicLink).toBeInTheDocument(); }); - test('renders without portal name', async () => { + test('without name field', async () => { const { ghostApi, popupFrame, triggerButtonFrame, emailInput, nameInput, signinButton, submitButton, siteTitle, popupIframeDocument, freePlanTitle, monthlyPlanTitle, yearlyPlanTitle, fullAccessTitle @@ -173,7 +243,7 @@ describe('Single tier site', () => { expect(magicLink).toBeInTheDocument(); }); - test('renders with only free plan', async () => { + test('with only free plan', async () => { let { ghostApi, popupFrame, triggerButtonFrame, emailInput, nameInput, signinButton, submitButton, siteTitle, popupIframeDocument, freePlanTitle, monthlyPlanTitle, yearlyPlanTitle, fullAccessTitle @@ -212,11 +282,211 @@ describe('Single tier site', () => { expect(magicLink).toBeInTheDocument(); }); }); + + describe('as paid member on single tier site', () => { + test('with default settings on monthly plan', async () => { + const { + ghostApi, popupFrame, triggerButtonFrame, emailInput, nameInput, signinButton, submitButton, + siteTitle, popupIframeDocument, freePlanTitle, monthlyPlanTitle, yearlyPlanTitle, fullAccessTitle + } = await setup({ + site: FixtureSite.singleTier.basic + }); + + expect(popupFrame).toBeInTheDocument(); + expect(triggerButtonFrame).toBeInTheDocument(); + expect(siteTitle).toBeInTheDocument(); + expect(emailInput).toBeInTheDocument(); + expect(nameInput).toBeInTheDocument(); + expect(freePlanTitle).toBeInTheDocument(); + expect(monthlyPlanTitle).toBeInTheDocument(); + expect(yearlyPlanTitle).toBeInTheDocument(); + expect(fullAccessTitle).toBeInTheDocument(); + expect(signinButton).toBeInTheDocument(); + expect(submitButton).toBeInTheDocument(); + + const monthlyPlanContainer = within(popupIframeDocument).queryByText(/Monthly$/); + const singleTierProduct = FixtureSite.singleTier.basic.products.find(p => p.type === 'paid'); + + const benefitText = singleTierProduct.benefits[0].name; + + fireEvent.change(nameInput, {target: {value: 'Jamie Larsen'}}); + fireEvent.change(emailInput, {target: {value: 'jamie@example.com'}}); + + fireEvent.click(monthlyPlanContainer.parentNode); + await within(popupIframeDocument).findByText(benefitText); + expect(emailInput).toHaveValue('jamie@example.com'); + expect(nameInput).toHaveValue('Jamie Larsen'); + fireEvent.click(submitButton); + expect(ghostApi.member.checkoutPlan).toHaveBeenLastCalledWith({ + email: 'jamie@example.com', + name: 'Jamie Larsen', + offerId: undefined, + plan: singleTierProduct.monthlyPrice.id + }); + }); + + test('with default settings on yearly plan', async () => { + const { + ghostApi, popupFrame, triggerButtonFrame, emailInput, nameInput, signinButton, submitButton, + siteTitle, popupIframeDocument, freePlanTitle, monthlyPlanTitle, yearlyPlanTitle, fullAccessTitle + } = await setup({ + site: FixtureSite.singleTier.basic + }); + + expect(popupFrame).toBeInTheDocument(); + expect(triggerButtonFrame).toBeInTheDocument(); + expect(siteTitle).toBeInTheDocument(); + expect(emailInput).toBeInTheDocument(); + expect(nameInput).toBeInTheDocument(); + expect(freePlanTitle).toBeInTheDocument(); + expect(monthlyPlanTitle).toBeInTheDocument(); + expect(yearlyPlanTitle).toBeInTheDocument(); + expect(fullAccessTitle).toBeInTheDocument(); + expect(signinButton).toBeInTheDocument(); + expect(submitButton).toBeInTheDocument(); + + const yearlyPlanContainer = within(popupIframeDocument).queryByText(/Yearly$/); + const singleTierProduct = FixtureSite.singleTier.basic.products.find(p => p.type === 'paid'); + + const benefitText = singleTierProduct.benefits[0].name; + + fireEvent.change(nameInput, {target: {value: 'Jamie Larsen'}}); + fireEvent.change(emailInput, {target: {value: 'jamie@example.com'}}); + + fireEvent.click(yearlyPlanContainer.parentNode); + await within(popupIframeDocument).findByText(benefitText); + expect(emailInput).toHaveValue('jamie@example.com'); + expect(nameInput).toHaveValue('Jamie Larsen'); + fireEvent.click(submitButton); + expect(ghostApi.member.checkoutPlan).toHaveBeenLastCalledWith({ + email: 'jamie@example.com', + name: 'Jamie Larsen', + offerId: undefined, + plan: singleTierProduct.yearlyPrice.id + }); + const magicLink = await within(popupIframeDocument).findByText(/now check your email/i); + expect(magicLink).toBeInTheDocument(); + }); + + test('without name field on monthly plan', async () => { + const { + ghostApi, popupFrame, triggerButtonFrame, emailInput, nameInput, signinButton, submitButton, + siteTitle, popupIframeDocument, freePlanTitle, monthlyPlanTitle, yearlyPlanTitle, fullAccessTitle + } = await setup({ + site: FixtureSite.singleTier.withoutName + }); + + const monthlyPlanContainer = within(popupIframeDocument).queryByText(/Monthly$/); + const singleTierProduct = FixtureSite.singleTier.basic.products.find(p => p.type === 'paid'); + const benefitText = singleTierProduct.benefits[0].name; + + expect(popupFrame).toBeInTheDocument(); + expect(triggerButtonFrame).toBeInTheDocument(); + expect(siteTitle).toBeInTheDocument(); + expect(emailInput).toBeInTheDocument(); + expect(nameInput).not.toBeInTheDocument(); + expect(freePlanTitle).toBeInTheDocument(); + expect(monthlyPlanTitle).toBeInTheDocument(); + expect(yearlyPlanTitle).toBeInTheDocument(); + expect(fullAccessTitle).toBeInTheDocument(); + expect(signinButton).toBeInTheDocument(); + expect(submitButton).toBeInTheDocument(); + + fireEvent.change(emailInput, {target: {value: 'jamie@example.com'}}); + + fireEvent.click(monthlyPlanContainer.parentNode); + await within(popupIframeDocument).findByText(benefitText); + + expect(emailInput).toHaveValue('jamie@example.com'); + fireEvent.click(submitButton); + + expect(ghostApi.member.checkoutPlan).toHaveBeenLastCalledWith({ + email: 'jamie@example.com', + name: '', + offerId: undefined, + plan: singleTierProduct.monthlyPrice.id + }); + }); + + test('with only paid plans available', async () => { + let { + ghostApi, popupFrame, triggerButtonFrame, emailInput, nameInput, signinButton, submitButton, + siteTitle, freePlanTitle, monthlyPlanTitle, yearlyPlanTitle + } = await setup({ + site: FixtureSite.singleTier.onlyPaidPlan + }); + + expect(popupFrame).toBeInTheDocument(); + expect(triggerButtonFrame).toBeInTheDocument(); + expect(siteTitle).toBeInTheDocument(); + expect(emailInput).toBeInTheDocument(); + expect(nameInput).toBeInTheDocument(); + expect(freePlanTitle).not.toBeInTheDocument(); + expect(monthlyPlanTitle).toBeInTheDocument(); + expect(yearlyPlanTitle).toBeInTheDocument(); + expect(signinButton).toBeInTheDocument(); + expect(submitButton).toBeInTheDocument(); + + fireEvent.change(emailInput, {target: {value: 'jamie@example.com'}}); + fireEvent.change(nameInput, {target: {value: 'Jamie Larsen'}}); + + expect(emailInput).toHaveValue('jamie@example.com'); + expect(nameInput).toHaveValue('Jamie Larsen'); + + fireEvent.click(submitButton); + const singleTierProduct = FixtureSite.singleTier.basic.products.find(p => p.type === 'paid'); + + expect(ghostApi.member.checkoutPlan).toHaveBeenLastCalledWith({ + email: 'jamie@example.com', + name: 'Jamie Larsen', + offerId: undefined, + plan: singleTierProduct.monthlyPrice.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, + 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).toBeInTheDocument(); + expect(submitButton).toBeInTheDocument(); + expect(offerName).toBeInTheDocument(); + expect(offerDescription).toBeInTheDocument(); + + fireEvent.change(emailInput, {target: {value: 'jamie@example.com'}}); + fireEvent.change(nameInput, {target: {value: 'Jamie Larsen'}}); + + expect(emailInput).toHaveValue('jamie@example.com'); + fireEvent.click(submitButton); + + expect(ghostApi.member.checkoutPlan).toHaveBeenLastCalledWith({ + email: 'jamie@example.com', + name: 'Jamie Larsen', + offerId, + plan: planId + }); + + window.location.hash = ''; + }); + }); }); -describe('Multiple tiers site', () => { - describe('Signup page', () => { - test('renders with default settings', async () => { +describe('Signup', () => { + describe('as free member on multi tier site', () => { + test('with default settings', async () => { const { ghostApi, popupFrame, triggerButtonFrame, emailInput, nameInput, signinButton, submitButton, siteTitle, popupIframeDocument, freePlanTitle @@ -248,7 +518,7 @@ describe('Multiple tiers site', () => { expect(magicLink).toBeInTheDocument(); }); - test('renders without portal name', async () => { + test('without name field', async () => { const { ghostApi, popupFrame, triggerButtonFrame, emailInput, nameInput, signinButton, submitButton, siteTitle, popupIframeDocument, freePlanTitle @@ -281,7 +551,7 @@ describe('Multiple tiers site', () => { expect(magicLink).toBeInTheDocument(); }); - test('renders with only free plan', async () => { + test('with only free plan available', async () => { let { ghostApi, popupFrame, triggerButtonFrame, emailInput, nameInput, signinButton, submitButton, siteTitle, popupIframeDocument, freePlanTitle @@ -317,4 +587,84 @@ describe('Multiple tiers site', () => { expect(magicLink).toBeInTheDocument(); }); }); + + describe('as paid member on multi tier site', () => { + test('with default settings', async () => { + const { + ghostApi, popupFrame, triggerButtonFrame, emailInput, nameInput, signinButton, submitButton, + siteTitle, popupIframeDocument, freePlanTitle + } = await multiTierSetup({ + site: FixtureSite.multipleTiers.basic + }); + + const firstPaidTier = FixtureSite.singleTier.basic.products.find(p => p.type === 'paid'); + + const regex = new RegExp(`${firstPaidTier.name}$`); + const tierContainer = within(popupIframeDocument).queryAllByText(regex); + + expect(popupFrame).toBeInTheDocument(); + expect(triggerButtonFrame).toBeInTheDocument(); + expect(siteTitle).toBeInTheDocument(); + expect(emailInput).toBeInTheDocument(); + expect(nameInput).toBeInTheDocument(); + expect(freePlanTitle[0]).toBeInTheDocument(); + expect(signinButton).toBeInTheDocument(); + expect(submitButton).toBeInTheDocument(); + + fireEvent.change(nameInput, {target: {value: 'Jamie Larsen'}}); + fireEvent.change(emailInput, {target: {value: 'jamie@example.com'}}); + + expect(emailInput).toHaveValue('jamie@example.com'); + expect(nameInput).toHaveValue('Jamie Larsen'); + + fireEvent.click(tierContainer[0]); + const labelText = popupIframeDocument.querySelector('.gh-portal-discount-label'); + await waitFor(() => { + expect(labelText).toBeInTheDocument(); + }); + + // added fake timeout for react state delay in setting plan + await new Promise(r => setTimeout(r, 10)); + fireEvent.click(submitButton); + await waitFor(() => expect(ghostApi.member.checkoutPlan).toHaveBeenCalledTimes(1)); + }); + + 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, + 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).toBeInTheDocument(); + expect(submitButton).toBeInTheDocument(); + expect(offerName).toBeInTheDocument(); + expect(offerDescription).toBeInTheDocument(); + + fireEvent.change(emailInput, {target: {value: 'jamie@example.com'}}); + fireEvent.change(nameInput, {target: {value: 'Jamie Larsen'}}); + + expect(emailInput).toHaveValue('jamie@example.com'); + fireEvent.click(submitButton); + + expect(ghostApi.member.checkoutPlan).toHaveBeenLastCalledWith({ + email: 'jamie@example.com', + name: 'Jamie Larsen', + 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 40ecdcfe81..d383715a77 100644 --- a/ghost/portal/src/utils/test-fixtures.js +++ b/ghost/portal/src/utils/test-fixtures.js @@ -117,6 +117,10 @@ export const site = { ...baseSingleTierSite, portal_plans: ['free'] }, + onlyPaidPlan: { + ...baseSingleTierSite, + portal_plans: ['monthly', 'yearly'] + }, withoutName: { ...baseSingleTierSite, portal_name: false @@ -136,7 +140,7 @@ export const site = { }; export const offer = getOfferData({ - tierId: baseSingleTierSite.products[0]?.id + tierId: singleSiteTier.find(p => p.type === 'paid')?.id }); export const member = {