0
Fork 0
mirror of https://github.com/TryGhost/Ghost.git synced 2025-04-01 02:41:39 -05:00

Added basic functional app flow tests

refs https://github.com/TryGhost/Team/issues/1345

- updates App setup to take custom api prop
- adds new signup flow test to check functional behavior of App for specific site settings
- cleanup
This commit is contained in:
Rishabh 2022-02-14 23:27:16 +05:30
parent 58781eb79e
commit 83a4a0e62a
6 changed files with 379 additions and 21 deletions

View file

@ -43,13 +43,9 @@ export default class App extends React.Component {
constructor(props) {
super(props);
if (!props.testState) {
// Setup custom trigger button handling
this.setupCustomTriggerButton();
}
this.setupCustomTriggerButton(props);
// testState is used by App.test to pass custom default state for testing
this.state = props.testState || {
this.state = {
site: null,
member: null,
page: 'loading',
@ -62,10 +58,7 @@ export default class App extends React.Component {
}
componentDidMount() {
/** Ignores API init when in test mode */
if (!this.props.testState) {
this.initSetup();
}
this.initSetup();
}
componentDidUpdate(prevProps, prevState) {
@ -104,7 +97,10 @@ export default class App extends React.Component {
}
/** Setup custom trigger buttons handling on page */
setupCustomTriggerButton() {
setupCustomTriggerButton(props) {
if (hasMode(['test'])) {
return;
}
// Handler for custom buttons
this.clickHandler = (event) => {
event.preventDefault();
@ -134,7 +130,7 @@ export default class App extends React.Component {
handleCustomTriggerClassUpdate() {
const popupOpenClass = 'gh-portal-open';
const popupCloseClass = 'gh-portal-close';
this.customTriggerButtons.forEach((customButton) => {
this.customTriggerButtons?.forEach((customButton) => {
const elAddClass = this.state.showPopup ? popupOpenClass : popupCloseClass;
const elRemoveClass = this.state.showPopup ? popupCloseClass : popupOpenClass;
customButton.classList.add(elAddClass);
@ -159,6 +155,7 @@ export default class App extends React.Component {
action: 'init:success',
initStatus: 'success'
};
this.handleSignupQuery({site, pageQuery, member});
this.setState(state);
@ -187,7 +184,6 @@ export default class App extends React.Component {
const {site: previewSiteData, ...restPreviewData} = this.fetchPreviewData();
const {site: notificationSiteData, ...restNotificationData} = this.fetchNotificationData();
let page = '';
return {
member,
page,
@ -216,6 +212,13 @@ export default class App extends React.Component {
if (hasMode(['dev']) && !this.state.customSiteUrl) {
return DEV_MODE_DATA;
}
// Setup test mode data
if (hasMode(['test'])) {
return {
showPopup: true
};
}
return {};
}
@ -432,8 +435,9 @@ export default class App extends React.Component {
/** Fetch site and member session data with Ghost Apis */
async fetchApiData() {
const {siteUrl, customSiteUrl} = this.props;
try {
this.GhostApi = setupGhostApi({siteUrl});
this.GhostApi = this.props.api || setupGhostApi({siteUrl});
const {site, member} = await this.GhostApi.init();
const colorOverride = this.getColorOverride();
@ -448,12 +452,16 @@ export default class App extends React.Component {
if (hasMode(['dev', 'test'], {customSiteUrl})) {
return {};
}
throw e;
}
}
/** Setup Sentry */
setupSentry({site}) {
if (hasMode(['test'])) {
return null;
}
const {portal_sentry: portalSentry, portal_version: portalVersion, version: ghostVersion} = site;
const appVersion = process.env.REACT_APP_VERSION || portalVersion;
const releaseTag = `portal@${appVersion}|ghost@${ghostVersion}`;
@ -477,6 +485,9 @@ export default class App extends React.Component {
/** Setup Firstpromoter script */
setupFirstPromoter({site, member}) {
if (hasMode(['test'])) {
return null;
}
const firstPromoterId = getFirstpromoterId({site});
const siteDomain = getSiteDomain({site});
if (firstPromoterId && siteDomain) {

View file

@ -3,7 +3,7 @@ import {render} from '@testing-library/react';
import {site} from './utils/fixtures';
import App from './App';
const setup = (overrides) => {
const setup = async (overrides) => {
const testState = {
site,
member: null,
@ -16,8 +16,9 @@ const setup = (overrides) => {
const {...utils} = render(
<App testState={testState} />
);
const triggerButtonFrame = utils.getByTitle(/portal-trigger/i);
const popupFrame = utils.getByTitle(/portal-popup/i);
const triggerButtonFrame = await utils.findByTitle(/portal-trigger/i);
const popupFrame = await utils.findByTitle(/portal-popup/i);
return {
popupFrame,
triggerButtonFrame,
@ -25,11 +26,11 @@ const setup = (overrides) => {
};
};
describe('App', () => {
test('renders popup and trigger frames', () => {
const {popupFrame, triggerButtonFrame} = setup();
describe.skip('App', () => {
test('renders popup and trigger frames', async () => {
const {popupFrame, triggerButtonFrame} = await setup();
expect(popupFrame).toBeInTheDocument();
expect(triggerButtonFrame).toBeInTheDocument();
});
});
});

View file

@ -3,6 +3,9 @@
const {getQueryPrice} = require('./utils/helpers');
function handleDataAttributes({siteUrl, site, member}) {
if (!siteUrl) {
return;
}
siteUrl = siteUrl.replace(/\/$/, '');
Array.prototype.forEach.call(document.querySelectorAll('form[data-members-form]'), function (form) {
let errorEl = form.querySelector('[data-members-error]');

View file

@ -0,0 +1,166 @@
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 setupGhostApi from '../utils/api.js';
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');
});
const utils = appRender(
<App api={ghostApi} />
);
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');
return {
ghostApi,
popupIframeDocument,
popupFrame,
triggerButtonFrame,
siteTitle,
emailInput,
nameInput,
signinButton,
submitButton,
freePlanTitle,
monthlyPlanTitle,
yearlyPlanTitle,
fullAccessTitle,
...utils
};
};
describe('App with tiers disabled', () => {
describe('Signup page', () => {
test('renders basic signup page', async () => {
const {
ghostApi, popupFrame, triggerButtonFrame, emailInput, nameInput, signinButton, submitButton,
siteTitle, popupIframeDocument, freePlanTitle, monthlyPlanTitle, yearlyPlanTitle, fullAccessTitle
} = await setup({
site: FixtureSite.tiersDisabled.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();
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(submitButton);
expect(ghostApi.member.sendMagicLink).toHaveBeenLastCalledWith({
email: 'jamie@example.com',
name: 'Jamie Larsen',
plan: 'free'
});
const magicLink = await within(popupIframeDocument).findByText(/now check your email/i);
expect(magicLink).toBeInTheDocument();
});
test('renders signup page without name', async () => {
const {
ghostApi, popupFrame, triggerButtonFrame, emailInput, nameInput, signinButton, submitButton,
siteTitle, popupIframeDocument, freePlanTitle, monthlyPlanTitle, yearlyPlanTitle, fullAccessTitle
} = await setup({
site: FixtureSite.tiersDisabled.withoutName
});
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'}});
expect(emailInput).toHaveValue('jamie@example.com');
fireEvent.click(submitButton);
expect(ghostApi.member.sendMagicLink).toHaveBeenLastCalledWith({
email: 'jamie@example.com',
name: '',
plan: 'free'
});
// Check if magic link page is shown
const magicLink = await within(popupIframeDocument).findByText(/now check your email/i);
expect(magicLink).toBeInTheDocument();
});
test('renders signup page with only free plan', async () => {
let {
ghostApi, popupFrame, triggerButtonFrame, emailInput, nameInput, signinButton, submitButton,
siteTitle, popupIframeDocument, freePlanTitle, monthlyPlanTitle, yearlyPlanTitle, fullAccessTitle
} = await setup({
site: FixtureSite.tiersDisabled.onlyFreePlan
});
expect(popupFrame).toBeInTheDocument();
expect(triggerButtonFrame).toBeInTheDocument();
expect(siteTitle).toBeInTheDocument();
expect(emailInput).toBeInTheDocument();
expect(nameInput).toBeInTheDocument();
expect(freePlanTitle).not.toBeInTheDocument();
expect(monthlyPlanTitle).not.toBeInTheDocument();
expect(yearlyPlanTitle).not.toBeInTheDocument();
expect(fullAccessTitle).not.toBeInTheDocument();
expect(signinButton).toBeInTheDocument();
expect(submitButton).not.toBeInTheDocument();
submitButton = within(popupIframeDocument).queryByRole('button', {name: 'Sign up'});
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);
expect(ghostApi.member.sendMagicLink).toHaveBeenLastCalledWith({
email: 'jamie@example.com',
name: 'Jamie Larsen',
plan: 'free'
});
// Check if magic link page is shown
const magicLink = await within(popupIframeDocument).findByText(/now check your email/i);
expect(magicLink).toBeInTheDocument();
});
});
});

View file

@ -0,0 +1,167 @@
/* eslint-disable no-unused-vars*/
import {getFreeProduct, getMemberData, getOfferData, getPriceData, getProductData, getSiteData, getSubscriptionData, getTestSite} from './fixtures-generator';
export const testSite = getTestSite();
const products = [
getFreeProduct({
name: 'Free',
// description: 'Free tier description which is actually a pretty long description',
description: '',
numOfBenefits: 0
})
,
getProductData({
name: 'Bronze',
// description: 'Access to all members articles',
description: '',
monthlyPrice: getPriceData({
interval: 'month',
amount: 700
}),
yearlyPrice: getPriceData({
interval: 'year',
amount: 7000
}),
numOfBenefits: 2
})
// ,
// getProductData({
// name: 'Silver',
// description: 'Access to all members articles and weekly podcast',
// monthlyPrice: getPriceData({
// interval: 'month',
// amount: 1200
// }),
// yearlyPrice: getPriceData({
// interval: 'year',
// amount: 12000
// }),
// numOfBenefits: 3
// })
// getProductData({
// name: 'Friends of the Blueprint',
// description: 'Get access to everything and lock in early adopter pricing for life + listen to my podcast',
// monthlyPrice: getPriceData({
// interval: 'month',
// amount: 18000
// }),
// yearlyPrice: getPriceData({
// interval: 'year',
// amount: 17000
// }),
// numOfBenefits: 4
// })
];
const basicSite = getSiteData({
title: 'The Blueprint',
description: 'Thoughts, stories and ideas.',
logo: 'https://static.ghost.org/v4.0.0/images/ghost-orb-1.png',
icon: 'https://static.ghost.org/v4.0.0/images/ghost-orb-1.png',
accentColor: '#45C32E',
url: 'https://portal.localhost',
plans: {
monthly: 5000,
yearly: 150000,
currency: 'USD'
},
// Simulate pre-multiple-tiers state:
// products: [products.find(d => d.type === 'paid')],
// portalProducts: null,
// Simulate multiple-tiers state:
products,
portalProducts: products.map(p => p.id),
//
allowSelfSignup: true,
membersSignupAccess: 'all',
freePriceName: 'Free',
freePriceDescription: 'Free preview',
isStripeConfigured: true,
portalButton: true,
portalName: true,
portalPlans: ['free', 'monthly', 'yearly'],
portalButtonIcon: 'icon-1',
portalButtonSignupText: 'Subscribe now',
portalButtonStyle: 'icon-and-text',
membersSupportAddress: 'support@example.com'
});
const tiersDisabledSite = {
...basicSite,
portal_products: undefined,
products: [products.find(d => d.type === 'paid')]
};
export const site = {
tiersDisabled: {
basic: tiersDisabledSite,
onlyFreePlan: {
...tiersDisabledSite,
portal_plans: 'free'
},
withoutName: {
...tiersDisabledSite,
portal_name: false
}
}
};
export const offer = getOfferData({
tierId: basicSite.products[0]?.id
});
export const member = {
free: getMemberData({
name: 'Jamie Larson',
email: 'jamie@example.com',
firstname: 'Jamie',
subscriptions: [],
paid: false,
avatarImage: '',
subscribed: true
}),
paid: getMemberData({
paid: true,
subscriptions: [
getSubscriptionData({
status: 'active',
currency: 'USD',
interval: 'year',
amount: 5000,
cardLast4: '4242',
startDate: '2021-10-05T03:18:30.000Z',
currentPeriodEnd: '2022-10-05T03:18:30.000Z',
cancelAtPeriodEnd: false
})
]
}),
complimentary: getMemberData({
paid: true,
subscriptions: []
}),
complimentaryWithSubscription: getMemberData({
paid: true,
subscriptions: [
getSubscriptionData({
amount: 0
})
]
}),
preview: getMemberData({
paid: true,
subscriptions: [
getSubscriptionData({
amount: 1500,
startDate: '2019-05-01T11:42:40.000Z',
currentPeriodEnd: '2021-06-05T11:42:40.000Z'
})
]
})
};
/* eslint-enable no-unused-vars*/

View file

@ -34,6 +34,16 @@ const customRender = (ui, {options = {}, overrideContext = {}} = {}) => {
};
};
export const appRender = (ui, {options = {}} = {}) => {
const mockOnActionFn = jest.fn();
const utils = render(ui, options);
return {
...utils,
mockOnActionFn
};
};
// re-export everything
export * from '@testing-library/react';