mirror of
https://github.com/TryGhost/Ghost.git
synced 2025-04-01 02:41:39 -05:00
Added tests for signup via data attributes
closes https://github.com/TryGhost/Team/issues/1365
This commit is contained in:
parent
b530672177
commit
a8448033fd
3 changed files with 307 additions and 120 deletions
|
@ -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 = {
|
||||
|
|
|
@ -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
|
||||
};
|
||||
|
|
174
ghost/portal/src/tests/data-attributes.test.js
Normal file
174
ghost/portal/src/tests/data-attributes.test.js
Normal file
|
@ -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'
|
||||
});
|
||||
});
|
||||
});
|
Loading…
Add table
Reference in a new issue