From a8448033fdeab698f2da906c6de179df56958433 Mon Sep 17 00:00:00 2001 From: Rishabh Date: Thu, 17 Feb 2022 10:25:51 +0530 Subject: [PATCH] Added tests for signup via data attributes closes https://github.com/TryGhost/Team/issues/1365 --- ghost/portal/src/App.js | 2 +- ghost/portal/src/data-attributes.js | 251 +++++++++--------- .../portal/src/tests/data-attributes.test.js | 174 ++++++++++++ 3 files changed, 307 insertions(+), 120 deletions(-) create mode 100644 ghost/portal/src/tests/data-attributes.test.js diff --git a/ghost/portal/src/App.js b/ghost/portal/src/App.js index 9f8ab66b20..ae37462c66 100644 --- a/ghost/portal/src/App.js +++ b/ghost/portal/src/App.js @@ -12,7 +12,7 @@ import './App.css'; import NotificationParser from './utils/notifications'; import {createPopupNotification, getCurrencySymbol, getFirstpromoterId, getPriceIdFromPageQuery, getProductFromId, getQueryPrice, getSiteDomain, isActiveOffer, isComplimentaryMember, isInviteOnlySite, isPaidMember, isSentryEventAllowed, removePortalLinkFromUrl} from './utils/helpers'; -const handleDataAttributes = require('./data-attributes'); +const {handleDataAttributes} = require('./data-attributes'); const React = require('react'); const DEV_MODE_DATA = { diff --git a/ghost/portal/src/data-attributes.js b/ghost/portal/src/data-attributes.js index 1572f5ec45..443af0de9c 100644 --- a/ghost/portal/src/data-attributes.js +++ b/ghost/portal/src/data-attributes.js @@ -2,6 +2,131 @@ const {getQueryPrice} = require('./utils/helpers'); +function formSubmitHandler({event, form, errorEl, siteUrl, submitHandler}) { + form.removeEventListener('submit', submitHandler); + event.preventDefault(); + if (errorEl) { + errorEl.innerText = ''; + } + form.classList.remove('success', 'invalid', 'error'); + let emailInput = event.target.querySelector('input[data-members-email]'); + let nameInput = event.target.querySelector('input[data-members-name]'); + let email = emailInput?.value; + let name = (nameInput && nameInput.value) || undefined; + let emailType = undefined; + let labels = []; + + let labelInputs = event.target.querySelectorAll('input[data-members-label]') || []; + for (let i = 0; i < labelInputs.length; ++i) { + labels.push(labelInputs[i].value); + } + + if (form.dataset.membersForm) { + emailType = form.dataset.membersForm; + } + + form.classList.add('loading'); + + fetch(`${siteUrl}/members/api/send-magic-link/`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + email: email, + emailType: emailType, + labels: labels, + name: name + }) + }).then(function (res) { + form.addEventListener('submit', submitHandler); + form.classList.remove('loading'); + if (res.ok) { + form.classList.add('success'); + } else { + if (errorEl) { + errorEl.innerText = 'There was an error sending the email, please try again'; + } + form.classList.add('error'); + } + }); +} + +function planClickHandler({event, el, errorEl, siteUrl, site, member, clickHandler}) { + el.removeEventListener('click', clickHandler); + event.preventDefault(); + let plan = el.dataset.membersPlan; + let priceId = ''; + if (plan) { + const price = getQueryPrice({site, priceId: plan.toLowerCase()}); + priceId = price ? price.id : plan; + } + let successUrl = el.dataset.membersSuccess; + let cancelUrl = el.dataset.membersCancel; + let checkoutSuccessUrl; + let checkoutCancelUrl; + + if (successUrl) { + checkoutSuccessUrl = (new URL(successUrl, window.location.href)).href; + } + + if (cancelUrl) { + checkoutCancelUrl = (new URL(cancelUrl, window.location.href)).href; + } + + if (errorEl) { + errorEl.innerText = ''; + } + el.classList.add('loading'); + const metadata = member ? { + checkoutType: 'upgrade' + } : {}; + return fetch(`${siteUrl}/members/api/session`, { + credentials: 'same-origin' + }).then(function (res) { + if (!res.ok) { + return null; + } + return res.text(); + }).then(function (identity) { + return fetch(`${siteUrl}/members/api/create-stripe-checkout-session/`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + priceId: priceId, + identity: identity, + successUrl: checkoutSuccessUrl, + cancelUrl: checkoutCancelUrl, + metadata + }) + }).then(function (res) { + if (!res.ok) { + throw new Error('Could not create stripe checkout session'); + } + return res.json(); + }); + }).then(function (result) { + let stripe = window.Stripe(result.publicKey); + return stripe.redirectToCheckout({ + sessionId: result.sessionId + }); + }).then(function (result) { + if (result.error) { + throw new Error(result.error.message); + } + }).catch(function (err) { + console.error(err); + el.addEventListener('click', clickHandler); + el.classList.remove('loading'); + if (errorEl) { + errorEl.innerText = err.message; + } + el.classList.add('error'); + }); +} + function handleDataAttributes({siteUrl, site, member}) { if (!siteUrl) { return; @@ -10,52 +135,7 @@ function handleDataAttributes({siteUrl, site, member}) { Array.prototype.forEach.call(document.querySelectorAll('form[data-members-form]'), function (form) { let errorEl = form.querySelector('[data-members-error]'); function submitHandler(event) { - form.removeEventListener('submit', submitHandler); - event.preventDefault(); - if (errorEl) { - errorEl.innerText = ''; - } - form.classList.remove('success', 'invalid', 'error'); - let emailInput = event.target.querySelector('input[data-members-email]'); - let nameInput = event.target.querySelector('input[data-members-name]'); - let email = emailInput?.value; - let name = (nameInput && nameInput.value) || undefined; - let emailType = undefined; - let labels = []; - - let labelInputs = event.target.querySelectorAll('input[data-members-label]') || []; - for (let i = 0; i < labelInputs.length; ++i) { - labels.push(labelInputs[i].value); - } - - if (form.dataset.membersForm) { - emailType = form.dataset.membersForm; - } - - form.classList.add('loading'); - fetch(`${siteUrl}/members/api/send-magic-link/`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ - email: email, - emailType: emailType, - labels: labels, - name: name - }) - }).then(function (res) { - form.addEventListener('submit', submitHandler); - form.classList.remove('loading'); - if (res.ok) { - form.classList.add('success'); - } else { - if (errorEl) { - errorEl.innerText = 'There was an error sending the email, please try again'; - } - form.classList.add('error'); - } - }); + formSubmitHandler({event, errorEl, form, siteUrl, submitHandler}); } form.addEventListener('submit', submitHandler); }); @@ -63,78 +143,7 @@ function handleDataAttributes({siteUrl, site, member}) { Array.prototype.forEach.call(document.querySelectorAll('[data-members-plan]'), function (el) { let errorEl = el.querySelector('[data-members-error]'); function clickHandler(event) { - el.removeEventListener('click', clickHandler); - event.preventDefault(); - let plan = el.dataset.membersPlan; - let priceId = ''; - if (plan) { - const price = getQueryPrice({site, priceId: plan.toLowerCase()}); - priceId = price ? price.id : plan; - } - let successUrl = el.dataset.membersSuccess; - let cancelUrl = el.dataset.membersCancel; - let checkoutSuccessUrl; - let checkoutCancelUrl; - - if (successUrl) { - checkoutSuccessUrl = (new URL(successUrl, window.location.href)).href; - } - - if (cancelUrl) { - checkoutCancelUrl = (new URL(cancelUrl, window.location.href)).href; - } - - if (errorEl) { - errorEl.innerText = ''; - } - el.classList.add('loading'); - const metadata = member ? { - checkoutType: 'upgrade' - } : {}; - fetch(`${siteUrl}/members/api/session`, { - credentials: 'same-origin' - }).then(function (res) { - if (!res.ok) { - return null; - } - return res.text(); - }).then(function (identity) { - return fetch(`${siteUrl}/members/api/create-stripe-checkout-session/`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ - priceId: priceId, - identity: identity, - successUrl: checkoutSuccessUrl, - cancelUrl: checkoutCancelUrl, - metadata - }) - }).then(function (res) { - if (!res.ok) { - throw new Error('Could not create stripe checkout session'); - } - return res.json(); - }); - }).then(function (result) { - let stripe = window.Stripe(result.publicKey); - return stripe.redirectToCheckout({ - sessionId: result.sessionId - }); - }).then(function (result) { - if (result.error) { - throw new Error(result.error.message); - } - }).catch(function (err) { - console.error(err); - el.addEventListener('click', clickHandler); - el.classList.remove('loading'); - if (errorEl) { - errorEl.innerText = err.message; - } - el.classList.add('error'); - }); + planClickHandler({el, event, errorEl, member, site, siteUrl, clickHandler}); } el.addEventListener('click', clickHandler); }); @@ -330,4 +339,8 @@ function handleDataAttributes({siteUrl, site, member}) { }); } -module.exports = handleDataAttributes; +module.exports = { + handleDataAttributes, + formSubmitHandler, + planClickHandler +}; diff --git a/ghost/portal/src/tests/data-attributes.test.js b/ghost/portal/src/tests/data-attributes.test.js new file mode 100644 index 0000000000..3a5233e0b6 --- /dev/null +++ b/ghost/portal/src/tests/data-attributes.test.js @@ -0,0 +1,174 @@ +import {site as FixturesSite, member as FixtureMember} from '../utils/test-fixtures'; +const {formSubmitHandler, planClickHandler} = require('../data-attributes'); + +// Mock data +function getMockData() { + const site = FixturesSite.singleTier.basic; + const member = null; + + const errorEl = { + innerText: '' + }; + const siteUrl = 'https://portal.localhost'; + const submitHandler = () => {}; + const clickHandler = () => {}; + const form = { + removeEventListener: () => {}, + classList: { + remove: () => {}, + add: () => {} + }, + dataset: { + membersForm: 'signup' + }, + addEventListener: () => {} + }; + + const element = { + removeEventListener: () => {}, + dataset: { + membersPlan: 'monthly', + membersSuccess: 'https://portal.localhost/success', + membersCancel: 'https://portal.localhost/cancel' + }, + classList: { + remove: () => {}, + add: () => {} + }, + addEventListener: () => {} + }; + + const event = { + preventDefault: () => {}, + target: { + querySelector: (elem) => { + if (elem === 'input[data-members-email]') { + return { + value: 'jamie@example.com' + }; + } + if (elem === 'input[data-members-name]') { + return { + value: 'Jamie Larsen' + }; + } + }, + querySelectorAll: (elem) => { + if (elem === 'input[data-members-label]') { + return [{ + value: 'Gold' + }]; + } + } + } + }; + + return { + event, form, siteUrl, submitHandler, errorEl, clickHandler, site, member, element + }; +} + +describe('Data attributes:', () => { + beforeEach(() => { + // Mock global fetch + jest.spyOn(window, 'fetch').mockImplementation((url) => { + if (url.includes('send-magic-link')) { + return Promise.resolve({ + ok: true, + json: async () => ({success: true}) + }); + } + + if (url.includes('api/session')) { + return Promise.resolve({ + ok: true, + text: async () => { + return 'session-identity'; + } + }); + } + + if (url.includes('create-stripe-checkout-session')) { + return Promise.resolve({ + ok: true, + json: async () => { + return { + publicKey: 'key-xyz' + }; + } + }); + } + return Promise.resolve({}); + }); + + // Mock global Stripe + window.Stripe = () => {}; + jest.spyOn(window, 'Stripe').mockImplementation(() => { + return { + redirectToCheckout: () => { + return Promise.resolve({}); + } + }; + }); + + // Mock window.location + let locationMock = jest.fn(); + delete window.location; + window.location = {assign: locationMock}; + window.location.href = (new URL('https://portal.localhost')).href; + }); + afterEach(() => { + jest.restoreAllMocks(); + }); + test('data-members-form: allows free signup', () => { + const {event, form, errorEl, siteUrl, submitHandler} = getMockData(); + + formSubmitHandler({event, form, errorEl, siteUrl, submitHandler}); + + expect(window.fetch).toHaveBeenCalledTimes(1); + + expect(window.fetch).toHaveBeenCalledWith('https://portal.localhost/members/api/send-magic-link/', {body: '{"email":"jamie@example.com","emailType":"signup","labels":["Gold"],"name":"Jamie Larsen"}', headers: {'Content-Type': 'application/json'}, method: 'POST'}); + }); + + test('data-members-plan: allows new member paid signup via direct checkout', async () => { + const {event, errorEl, siteUrl, clickHandler, site, member, element} = getMockData(); + + const paidTier = site.products.find(p => p.type === 'paid'); + const plan = paidTier.monthlyPrice.id; + + await planClickHandler({event, errorEl, siteUrl, clickHandler, site, member, el: element}); + expect(window.fetch).toHaveBeenNthCalledWith(1, + 'https://portal.localhost/members/api/session', { + credentials: 'same-origin' + } + ); + expect(window.fetch).toHaveBeenNthCalledWith(2, + 'https://portal.localhost/members/api/create-stripe-checkout-session/', { + body: `{"priceId":"${plan}","identity":"session-identity","successUrl":"https://portal.localhost/success","cancelUrl":"https://portal.localhost/cancel","metadata":{}}`, + headers: { + 'Content-Type': 'application/json' + }, + method: 'POST' + } + ); + }); + + test('data-members-plan: allows free member upgrade via direct checkout', async () => { + let {event, errorEl, siteUrl, clickHandler, site, member, element} = getMockData(); + member = FixtureMember.free; + const paidTier = site.products.find(p => p.type === 'paid'); + const plan = paidTier.monthlyPrice.id; + + await planClickHandler({event, errorEl, siteUrl, clickHandler, site, member, el: element}); + expect(window.fetch).toHaveBeenNthCalledWith(1, 'https://portal.localhost/members/api/session', { + credentials: 'same-origin' + }); + expect(window.fetch).toHaveBeenNthCalledWith(2, 'https://portal.localhost/members/api/create-stripe-checkout-session/', { + body: `{"priceId":"${plan}","identity":"session-identity","successUrl":"https://portal.localhost/success","cancelUrl":"https://portal.localhost/cancel","metadata":{"checkoutType":"upgrade"}}`, + headers: { + 'Content-Type': 'application/json' + }, + method: 'POST' + }); + }); +});