mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-12-24 03:14:03 +03:00
Added 100% test coverage to email renderer and fixed authors bug
refs https://github.com/TryGhost/Team/issues/2339 This fixed a bug in the new email flow that more than 2 authors were displayed as 'undefined & 2 others'.
This commit is contained in:
parent
a3f7188369
commit
6a664d11b9
@ -309,7 +309,7 @@ class EmailRenderer {
|
||||
* Takes a member and newsletter uuid. Returns the url that should be used to unsubscribe
|
||||
* In case of no member uuid, generates the preview unsubscribe url - `?preview=1`
|
||||
*
|
||||
* @param {string} uuid post uuid
|
||||
* @param {string} [uuid] post uuid
|
||||
* @param {Object} [options]
|
||||
* @param {string} [options.newsletterUuid] newsletter uuid
|
||||
* @param {boolean} [options.comments] Unsubscribe from comment emails
|
||||
@ -499,9 +499,16 @@ class EmailRenderer {
|
||||
* @private
|
||||
*/
|
||||
async getTemplateData({post, newsletter, html, addPaywall}) {
|
||||
const accentColor = this.#settingsCache.get('accent_color') || '#15212A';
|
||||
const adjustedAccentColor = accentColor && darkenToContrastThreshold(accentColor, '#ffffff', 2).hex();
|
||||
const adjustedAccentContrastColor = accentColor && textColorForBackgroundColor(adjustedAccentColor).hex();
|
||||
let accentColor = this.#settingsCache.get('accent_color') || '#15212A';
|
||||
let adjustedAccentColor;
|
||||
let adjustedAccentContrastColor;
|
||||
try {
|
||||
adjustedAccentColor = accentColor && darkenToContrastThreshold(accentColor, '#ffffff', 2).hex();
|
||||
adjustedAccentContrastColor = accentColor && textColorForBackgroundColor(adjustedAccentColor).hex();
|
||||
} catch (e) {
|
||||
logging.error(e);
|
||||
accentColor = '#15212A';
|
||||
}
|
||||
|
||||
const {href: headerImage, width: headerImageWidth} = await this.limitImageWidth(newsletter.get('header_image'));
|
||||
const {href: postFeatureImage, width: postFeatureImageWidth} = await this.limitImageWidth(post.get('feature_image'));
|
||||
@ -515,11 +522,11 @@ class EmailRenderer {
|
||||
|
||||
let authors;
|
||||
const postAuthors = await post.getLazyRelation('authors');
|
||||
if (postAuthors.models) {
|
||||
if (postAuthors?.models) {
|
||||
if (postAuthors.models.length <= 2) {
|
||||
authors = postAuthors.models.map(author => author.get('name')).join(' & ');
|
||||
} else {
|
||||
authors = `${postAuthors.models[0].name} & ${postAuthors.models.length - 1} others`;
|
||||
authors = `${postAuthors.models[0].get('name')} & ${postAuthors.models.length - 1} others`;
|
||||
}
|
||||
}
|
||||
|
||||
@ -579,7 +586,7 @@ class EmailRenderer {
|
||||
showHeaderIcon: newsletter.get('show_header_icon') && this.#settingsCache.get('icon'),
|
||||
showHeaderTitle: newsletter.get('show_header_title'),
|
||||
showHeaderName: newsletter.get('show_header_name'),
|
||||
showFeatureImage: newsletter.get('show_feature_image') && postFeatureImage,
|
||||
showFeatureImage: newsletter.get('show_feature_image') && !!postFeatureImage,
|
||||
footerContent: newsletter.get('footer_content'),
|
||||
|
||||
classes: {
|
||||
|
@ -1,25 +1,45 @@
|
||||
const EmailRenderer = require('../lib/email-renderer');
|
||||
const {EmailRenderer} = require('../');
|
||||
const assert = require('assert');
|
||||
const cheerio = require('cheerio');
|
||||
const {createModel} = require('./utils');
|
||||
const linkReplacer = require('@tryghost/link-replacer');
|
||||
const sinon = require('sinon');
|
||||
const logging = require('@tryghost/logging');
|
||||
|
||||
describe('Email renderer', function () {
|
||||
describe('buildReplacementDefinitions', function () {
|
||||
const emailRenderer = new EmailRenderer({
|
||||
urlUtils: {
|
||||
urlFor: () => 'http://example.com'
|
||||
}
|
||||
});
|
||||
const newsletter = {
|
||||
get: () => '123'
|
||||
};
|
||||
const member = {
|
||||
id: '456',
|
||||
uuid: 'myuuid',
|
||||
name: 'Test User',
|
||||
email: 'test@example.com'
|
||||
};
|
||||
let logStub;
|
||||
|
||||
it('returns an empty list of replacemetns if none used', function () {
|
||||
beforeEach(function () {
|
||||
logStub = sinon.stub(logging, 'error');
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
sinon.restore();
|
||||
});
|
||||
|
||||
describe('buildReplacementDefinitions', function () {
|
||||
let emailRenderer;
|
||||
let newsletter;
|
||||
let member;
|
||||
|
||||
beforeEach(function () {
|
||||
emailRenderer = new EmailRenderer({
|
||||
urlUtils: {
|
||||
urlFor: () => 'http://example.com/subdirectory'
|
||||
}
|
||||
});
|
||||
newsletter = createModel({
|
||||
uuid: 'newsletteruuid'
|
||||
});
|
||||
member = {
|
||||
id: '456',
|
||||
uuid: 'myuuid',
|
||||
name: 'Test User',
|
||||
email: 'test@example.com'
|
||||
};
|
||||
});
|
||||
|
||||
it('returns an empty list of replacements if nothing is used', function () {
|
||||
const html = 'Hello world';
|
||||
const replacements = emailRenderer.buildReplacementDefinitions({html, newsletter});
|
||||
assert.equal(replacements.length, 0);
|
||||
@ -52,6 +72,15 @@ describe('Email renderer', function () {
|
||||
assert.equal(replacements[0].getValue(member), 'Test');
|
||||
});
|
||||
|
||||
it('returns correct unsubscribe url', function () {
|
||||
const html = 'Hello %%{unsubscribe_url}%%,';
|
||||
const replacements = emailRenderer.buildReplacementDefinitions({html, newsletter});
|
||||
assert.equal(replacements.length, 1);
|
||||
assert.equal(replacements[0].token.toString(), '/%%\\{unsubscribe_url\\}%%/g');
|
||||
assert.equal(replacements[0].id, 'unsubscribe_url');
|
||||
assert.equal(replacements[0].getValue(member), `http://example.com/subdirectory/unsubscribe/?uuid=myuuid&newsletter=newsletteruuid`);
|
||||
});
|
||||
|
||||
it('supports fallback values', function () {
|
||||
const html = 'Hey %%{first_name, "there"}%%,';
|
||||
const replacements = emailRenderer.buildReplacementDefinitions({html, newsletter});
|
||||
@ -91,7 +120,7 @@ describe('Email renderer', function () {
|
||||
});
|
||||
});
|
||||
|
||||
describe('getPost', function () {
|
||||
describe('getSubject', function () {
|
||||
const emailRenderer = new EmailRenderer({
|
||||
urlUtils: {
|
||||
urlFor: () => 'http://example.com'
|
||||
@ -99,46 +128,37 @@ describe('Email renderer', function () {
|
||||
});
|
||||
|
||||
it('returns a post with correct subject from meta', function () {
|
||||
let post = {
|
||||
related: () => {
|
||||
return {
|
||||
get: () => {
|
||||
return 'Test Newsletter';
|
||||
}
|
||||
};
|
||||
},
|
||||
get: () => {
|
||||
return 'Sample Newsletter';
|
||||
}
|
||||
};
|
||||
const post = createModel({
|
||||
posts_meta: createModel({
|
||||
email_subject: 'Test Newsletter'
|
||||
}),
|
||||
title: 'Sample Post',
|
||||
loaded: ['posts_meta']
|
||||
});
|
||||
let response = emailRenderer.getSubject(post);
|
||||
response.should.equal('Test Newsletter');
|
||||
});
|
||||
|
||||
it('returns a post with correct subject from title', function () {
|
||||
let post = {
|
||||
related: () => {
|
||||
return {
|
||||
get: () => {
|
||||
return '';
|
||||
}
|
||||
};
|
||||
},
|
||||
get: () => {
|
||||
return 'Sample Newsletter';
|
||||
}
|
||||
};
|
||||
const post = createModel({
|
||||
posts_meta: createModel({
|
||||
email_subject: ''
|
||||
}),
|
||||
title: 'Sample Post',
|
||||
loaded: ['posts_meta']
|
||||
});
|
||||
let response = emailRenderer.getSubject(post);
|
||||
response.should.equal('Sample Newsletter');
|
||||
response.should.equal('Sample Post');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getFromAddress', function () {
|
||||
let siteTitle = 'Test Blog';
|
||||
let emailRenderer = new EmailRenderer({
|
||||
settingsCache: {
|
||||
get: (key) => {
|
||||
if (key === 'title') {
|
||||
return 'Test Blog';
|
||||
return siteTitle;
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -150,34 +170,41 @@ describe('Email renderer', function () {
|
||||
});
|
||||
|
||||
it('returns correct from address for newsletter', function () {
|
||||
let newsletter = {
|
||||
get: (key) => {
|
||||
if (key === 'sender_email') {
|
||||
return 'ghost@example.com';
|
||||
}
|
||||
|
||||
if (key === 'sender_name') {
|
||||
return 'Ghost';
|
||||
}
|
||||
}
|
||||
};
|
||||
let response = emailRenderer.getFromAddress({}, newsletter);
|
||||
const newsletter = createModel({
|
||||
sender_email: 'ghost@example.com',
|
||||
sender_name: 'Ghost'
|
||||
});
|
||||
const response = emailRenderer.getFromAddress({}, newsletter);
|
||||
response.should.equal('"Ghost" <ghost@example.com>');
|
||||
});
|
||||
|
||||
newsletter = {
|
||||
get: (key) => {
|
||||
if (key === 'sender_email') {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (key === 'sender_name') {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
};
|
||||
response = emailRenderer.getFromAddress({}, newsletter);
|
||||
it('defaults to site title and domain', function () {
|
||||
const newsletter = createModel({
|
||||
sender_email: '',
|
||||
sender_name: ''
|
||||
});
|
||||
const response = emailRenderer.getFromAddress({}, newsletter);
|
||||
response.should.equal('"Test Blog" <reply@example.com>');
|
||||
});
|
||||
|
||||
it('changes localhost domain to proper domain in development', function () {
|
||||
const newsletter = createModel({
|
||||
sender_email: 'example@localhost',
|
||||
sender_name: ''
|
||||
});
|
||||
const response = emailRenderer.getFromAddress({}, newsletter);
|
||||
response.should.equal('"Test Blog" <localhost@example.com>');
|
||||
});
|
||||
|
||||
it('ignores empty sender names', function () {
|
||||
siteTitle = '';
|
||||
const newsletter = createModel({
|
||||
sender_email: 'example@example.com',
|
||||
sender_name: ''
|
||||
});
|
||||
const response = emailRenderer.getFromAddress({}, newsletter);
|
||||
response.should.equal('example@example.com');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getReplyToAddress', function () {
|
||||
@ -192,29 +219,32 @@ describe('Email renderer', function () {
|
||||
settingsHelpers: {
|
||||
getMembersSupportAddress: () => {
|
||||
return 'support@example.com';
|
||||
},
|
||||
getNoReplyAddress: () => {
|
||||
return 'reply@example.com';
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it('returns correct reply to address for newsletter', function () {
|
||||
let newsletter = {
|
||||
get: (key) => {
|
||||
if (key === 'sender_email') {
|
||||
return 'ghost@example.com';
|
||||
}
|
||||
|
||||
if (key === 'sender_name') {
|
||||
return 'Ghost';
|
||||
}
|
||||
|
||||
if (key === 'sender_reply_to') {
|
||||
return 'support';
|
||||
}
|
||||
}
|
||||
};
|
||||
let response = emailRenderer.getReplyToAddress({}, newsletter);
|
||||
it('returns support address', function () {
|
||||
const newsletter = createModel({
|
||||
sender_email: 'ghost@example.com',
|
||||
sender_name: 'Ghost',
|
||||
sender_reply_to: 'support'
|
||||
});
|
||||
const response = emailRenderer.getReplyToAddress({}, newsletter);
|
||||
response.should.equal('support@example.com');
|
||||
});
|
||||
|
||||
it('returns correct reply to address for newsletter', function () {
|
||||
const newsletter = createModel({
|
||||
sender_email: 'ghost@example.com',
|
||||
sender_name: 'Ghost',
|
||||
sender_reply_to: 'newsletter'
|
||||
});
|
||||
const response = emailRenderer.getReplyToAddress({}, newsletter);
|
||||
response.should.equal(`"Ghost" <ghost@example.com>`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getSegments', function () {
|
||||
@ -305,16 +335,22 @@ describe('Email renderer', function () {
|
||||
});
|
||||
|
||||
describe('renderBody', function () {
|
||||
let renderedPost = '<p> Lexical Test</p>';
|
||||
let renderedPost = '<p>Lexical Test</p>';
|
||||
let emailRenderer = new EmailRenderer({
|
||||
audienceFeedbackService: {
|
||||
buildLink: () => {
|
||||
return new URL('http://example.com');
|
||||
buildLink: (_uuid, _postId, score) => {
|
||||
return new URL('http://feedback-link.com/?score=' + encodeURIComponent(score) + '&uuid=' + encodeURIComponent(_uuid));
|
||||
}
|
||||
},
|
||||
urlUtils: {
|
||||
urlFor: () => {
|
||||
return 'http://icon.example.com';
|
||||
urlFor: (type) => {
|
||||
if (type === 'image') {
|
||||
return 'http://icon.example.com';
|
||||
}
|
||||
return 'http://example.com/subdirectory';
|
||||
},
|
||||
isSiteUrl: (u) => {
|
||||
return u.hostname === 'example.com';
|
||||
}
|
||||
},
|
||||
settingsCache: {
|
||||
@ -347,70 +383,59 @@ describe('Email renderer', function () {
|
||||
return '<p> Mobiledoc Test</p>';
|
||||
}
|
||||
}
|
||||
},
|
||||
linkReplacer,
|
||||
memberAttributionService: {
|
||||
addEmailSourceAttributionTracking: (u, newsletter) => {
|
||||
u.searchParams.append('source_tracking', newsletter?.get('name') ?? 'site');
|
||||
return u;
|
||||
},
|
||||
addPostAttributionTracking: (u) => {
|
||||
u.searchParams.append('post_tracking', 'added');
|
||||
return u;
|
||||
}
|
||||
},
|
||||
linkTracking: {
|
||||
service: {
|
||||
addTrackingToUrl: (u, _post, uuid) => {
|
||||
return new URL('http://tracked-link.com/?m=' + encodeURIComponent(uuid) + '&url=' + encodeURIComponent(u.href));
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
let basePost;
|
||||
|
||||
it('returns correct empty segment for post', async function () {
|
||||
let post = {
|
||||
beforeEach(function () {
|
||||
basePost = {
|
||||
url: '',
|
||||
related: () => {
|
||||
return null;
|
||||
},
|
||||
get: (key) => {
|
||||
if (key === 'lexical') {
|
||||
return '{}';
|
||||
}
|
||||
|
||||
if (key === 'visibility') {
|
||||
return 'public';
|
||||
}
|
||||
|
||||
if (key === 'title') {
|
||||
return 'Test Post';
|
||||
}
|
||||
|
||||
if (key === 'plaintext') {
|
||||
return 'Test plaintext for post';
|
||||
}
|
||||
|
||||
if (key === 'custom_excerpt') {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
getLazyRelation: () => {
|
||||
return {
|
||||
models: [{
|
||||
get: (key) => {
|
||||
if (key === 'name') {
|
||||
return 'Test Author';
|
||||
}
|
||||
}
|
||||
}]
|
||||
};
|
||||
}
|
||||
lexical: '{}',
|
||||
visibility: 'public',
|
||||
title: 'Test Post',
|
||||
plaintext: 'Test plaintext for post',
|
||||
custom_excerpt: null,
|
||||
authors: [
|
||||
createModel({
|
||||
name: 'Test Author'
|
||||
})
|
||||
],
|
||||
posts_meta: createModel({
|
||||
feature_image_alt: null,
|
||||
feature_image_caption: null
|
||||
}),
|
||||
loaded: ['posts_meta']
|
||||
};
|
||||
let newsletter = {
|
||||
get: (key) => {
|
||||
if (key === 'header_image') {
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
if (key === 'name') {
|
||||
return 'Test Newsletter';
|
||||
}
|
||||
|
||||
if (key === 'badge') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (key === 'feedback_enabled') {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
};
|
||||
let segment = null;
|
||||
let options = {};
|
||||
it('returns feedback buttons and unsubcribe links', async function () {
|
||||
const post = createModel(basePost);
|
||||
const newsletter = createModel({
|
||||
header_image: null,
|
||||
name: 'Test Newsletter',
|
||||
show_badge: false,
|
||||
feedback_enabled: true
|
||||
});
|
||||
const segment = null;
|
||||
const options = {};
|
||||
|
||||
let response = await emailRenderer.renderBody(
|
||||
post,
|
||||
@ -422,19 +447,247 @@ describe('Email renderer', function () {
|
||||
const $ = cheerio.load(response.html);
|
||||
|
||||
response.plaintext.should.containEql('Test Post');
|
||||
|
||||
// Unsubscribe button included
|
||||
response.plaintext.should.containEql('Unsubscribe [%%{unsubscribe_url}%%]');
|
||||
response.plaintext.should.containEql('http://example.com');
|
||||
should($('.preheader').text()).eql('Test plaintext for post');
|
||||
response.html.should.containEql('Test Post');
|
||||
response.html.should.containEql('Unsubscribe');
|
||||
response.html.should.containEql('http://example.com');
|
||||
response.replacements.length.should.eql(1);
|
||||
response.replacements.length.should.eql(2);
|
||||
response.replacements.should.match([
|
||||
{
|
||||
id: 'uuid'
|
||||
},
|
||||
{
|
||||
id: 'unsubscribe_url',
|
||||
token: /%%\{unsubscribe_url\}%%/g
|
||||
}
|
||||
]);
|
||||
|
||||
response.plaintext.should.containEql('http://example.com');
|
||||
should($('.preheader').text()).eql('Test plaintext for post');
|
||||
response.html.should.containEql('Test Post');
|
||||
response.html.should.containEql('http://example.com');
|
||||
|
||||
// Does not include Ghost badge
|
||||
response.html.should.not.containEql('https://ghost.org/');
|
||||
|
||||
// Test feedback buttons included
|
||||
response.html.should.containEql('http://feedback-link.com/?score=1');
|
||||
response.html.should.containEql('http://feedback-link.com/?score=0');
|
||||
});
|
||||
|
||||
it('uses custom excerpt as preheader', async function () {
|
||||
const post = createModel({...basePost, custom_excerpt: 'Custom excerpt'});
|
||||
const newsletter = createModel({
|
||||
header_image: null,
|
||||
name: 'Test Newsletter',
|
||||
show_badge: false,
|
||||
feedback_enabled: true
|
||||
});
|
||||
const segment = null;
|
||||
const options = {};
|
||||
|
||||
let response = await emailRenderer.renderBody(
|
||||
post,
|
||||
newsletter,
|
||||
segment,
|
||||
options
|
||||
);
|
||||
|
||||
const $ = cheerio.load(response.html);
|
||||
should($('.preheader').text()).eql('Custom excerpt');
|
||||
});
|
||||
|
||||
it('only includes first author if more than 2', async function () {
|
||||
const post = createModel({...basePost, authors: [
|
||||
createModel({
|
||||
name: 'A'
|
||||
}),
|
||||
createModel({
|
||||
name: 'B'
|
||||
}),
|
||||
createModel({
|
||||
name: 'C'
|
||||
})
|
||||
]});
|
||||
const newsletter = createModel({
|
||||
header_image: null,
|
||||
name: 'Test Newsletter',
|
||||
show_badge: false,
|
||||
feedback_enabled: true
|
||||
});
|
||||
const segment = null;
|
||||
const options = {};
|
||||
|
||||
let response = await emailRenderer.renderBody(
|
||||
post,
|
||||
newsletter,
|
||||
segment,
|
||||
options
|
||||
);
|
||||
|
||||
assert.match(response.html, /By A & 2 others/);
|
||||
assert.match(response.plaintext, /By A & 2 others/);
|
||||
});
|
||||
|
||||
it('includes header icon, title, name', async function () {
|
||||
const post = createModel(basePost);
|
||||
const newsletter = createModel({
|
||||
header_image: null,
|
||||
name: 'Test Newsletter',
|
||||
show_badge: false,
|
||||
feedback_enabled: true,
|
||||
|
||||
show_header_icon: true,
|
||||
show_header_title: true,
|
||||
show_header_name: true
|
||||
});
|
||||
const segment = null;
|
||||
const options = {};
|
||||
|
||||
let response = await emailRenderer.renderBody(
|
||||
post,
|
||||
newsletter,
|
||||
segment,
|
||||
options
|
||||
);
|
||||
|
||||
response.html.should.containEql('http://icon.example.com');
|
||||
assert.match(response.html, /class="site-title"[^>]*?>Test Blog/);
|
||||
assert.match(response.html, /class="site-subtitle"[^>]*?>Test Newsletter/);
|
||||
});
|
||||
|
||||
it('includes header icon and name', async function () {
|
||||
const post = createModel(basePost);
|
||||
const newsletter = createModel({
|
||||
header_image: null,
|
||||
name: 'Test Newsletter',
|
||||
show_badge: false,
|
||||
feedback_enabled: true,
|
||||
|
||||
show_header_icon: true,
|
||||
show_header_title: false,
|
||||
show_header_name: true
|
||||
});
|
||||
const segment = null;
|
||||
const options = {};
|
||||
|
||||
let response = await emailRenderer.renderBody(
|
||||
post,
|
||||
newsletter,
|
||||
segment,
|
||||
options
|
||||
);
|
||||
|
||||
response.html.should.containEql('http://icon.example.com');
|
||||
assert.match(response.html, /class="site-title"[^>]*?>Test Newsletter/);
|
||||
});
|
||||
|
||||
it('includes Ghost badge if enabled', async function () {
|
||||
const post = createModel(basePost);
|
||||
const newsletter = createModel({
|
||||
header_image: null,
|
||||
name: 'Test Newsletter',
|
||||
show_badge: true,
|
||||
feedback_enabled: false
|
||||
});
|
||||
const segment = null;
|
||||
const options = {};
|
||||
|
||||
let response = await emailRenderer.renderBody(
|
||||
post,
|
||||
newsletter,
|
||||
segment,
|
||||
options
|
||||
);
|
||||
|
||||
// Does include include Ghost badge
|
||||
assert.match(response.html, /https:\/\/ghost.org\//);
|
||||
|
||||
// Test feedback buttons not included
|
||||
response.html.should.not.containEql('http://feedback-link.com/?score=1');
|
||||
response.html.should.not.containEql('http://feedback-link.com/?score=0');
|
||||
});
|
||||
|
||||
it('includes newsletter footer as raw html', async function () {
|
||||
const post = createModel(basePost);
|
||||
const newsletter = createModel({
|
||||
header_image: null,
|
||||
name: 'Test Newsletter',
|
||||
show_badge: true,
|
||||
feedback_enabled: false,
|
||||
footer_content: '<p>Test footer</p>'
|
||||
});
|
||||
const segment = null;
|
||||
const options = {};
|
||||
|
||||
let response = await emailRenderer.renderBody(
|
||||
post,
|
||||
newsletter,
|
||||
segment,
|
||||
options
|
||||
);
|
||||
|
||||
// Test footer
|
||||
response.html.should.containEql('Test footer</p>'); // begin tag skipped because style is inlined in that tag
|
||||
response.plaintext.should.containEql('Test footer');
|
||||
});
|
||||
|
||||
it('replaces all links except the unsubscribe and feedback links', async function () {
|
||||
const post = createModel(basePost);
|
||||
const newsletter = createModel({
|
||||
header_image: null,
|
||||
name: 'Test Newsletter',
|
||||
show_badge: true,
|
||||
feedback_enabled: true
|
||||
});
|
||||
const segment = null;
|
||||
const options = {
|
||||
clickTrackingEnabled: true
|
||||
};
|
||||
|
||||
renderedPost = '<p>Lexical Test</p><p><a href="https://external-domain.com/?ref=123">Hello</a><a href="https://example.com/?ref=123"><img src="example" /></a></p>';
|
||||
|
||||
let response = await emailRenderer.renderBody(
|
||||
post,
|
||||
newsletter,
|
||||
segment,
|
||||
options
|
||||
);
|
||||
|
||||
// Check all links have domain tracked-link.com
|
||||
const $ = cheerio.load(response.html);
|
||||
const links = [];
|
||||
for (const link of $('a').toArray()) {
|
||||
const href = $(link).attr('href');
|
||||
links.push(href);
|
||||
if (href.includes('unsubscribe_url')) {
|
||||
href.should.eql('%%{unsubscribe_url}%%');
|
||||
} else if (href.includes('feedback-link.com')) {
|
||||
href.should.containEql('%%{uuid}%%');
|
||||
} else {
|
||||
href.should.containEql('tracked-link.com');
|
||||
href.should.containEql('m=%%{uuid}%%');
|
||||
}
|
||||
}
|
||||
|
||||
// Update the following array when you make changes to the email template, check if replacements are correct for each newly added link.
|
||||
assert.deepEqual(links, [
|
||||
`http://tracked-link.com/?m=%%{uuid}%%&url=http%3A%2F%2Fexample.com%2F%3Fsource_tracking%3DTest%2BNewsletter%26post_tracking%3Dadded`,
|
||||
`http://tracked-link.com/?m=%%{uuid}%%&url=http%3A%2F%2Fexample.com%2F%3Fsource_tracking%3DTest%2BNewsletter%26post_tracking%3Dadded`,
|
||||
`http://tracked-link.com/?m=%%{uuid}%%&url=https%3A%2F%2Fexternal-domain.com%2F%3Fref%3D123%26source_tracking%3Dsite`,
|
||||
`http://tracked-link.com/?m=%%{uuid}%%&url=https%3A%2F%2Fexample.com%2F%3Fref%3D123%26source_tracking%3DTest%2BNewsletter%26post_tracking%3Dadded`,
|
||||
`http://feedback-link.com/?score=1&uuid=%%{uuid}%%`,
|
||||
`http://feedback-link.com/?score=0&uuid=%%{uuid}%%`,
|
||||
`%%{unsubscribe_url}%%`,
|
||||
`http://tracked-link.com/?m=%%{uuid}%%&url=https%3A%2F%2Fghost.org%2F%3Fsource_tracking%3Dsite`
|
||||
]);
|
||||
|
||||
// Check uuid in replacements
|
||||
response.replacements.length.should.eql(2);
|
||||
response.replacements[0].id.should.eql('uuid');
|
||||
response.replacements[0].token.should.eql(/%%\{uuid\}%%/g);
|
||||
response.replacements[1].id.should.eql('unsubscribe_url');
|
||||
response.replacements[1].token.should.eql(/%%\{unsubscribe_url\}%%/g);
|
||||
});
|
||||
|
||||
it('removes data-gh-segment and renders paywall', async function () {
|
||||
@ -504,8 +757,11 @@ describe('Email renderer', function () {
|
||||
response.html.should.containEql('Test Post');
|
||||
response.html.should.containEql('Unsubscribe');
|
||||
response.html.should.containEql('http://example.com');
|
||||
response.replacements.length.should.eql(1);
|
||||
response.replacements.length.should.eql(2);
|
||||
response.replacements.should.match([
|
||||
{
|
||||
id: 'uuid'
|
||||
},
|
||||
{
|
||||
id: 'unsubscribe_url',
|
||||
token: /%%\{unsubscribe_url\}%%/g
|
||||
@ -529,6 +785,153 @@ describe('Email renderer', function () {
|
||||
});
|
||||
});
|
||||
|
||||
describe('getTemplateData', function () {
|
||||
let settings = {};
|
||||
const emailRenderer = new EmailRenderer({
|
||||
audienceFeedbackService: {
|
||||
buildLink: (_uuid, _postId, score) => {
|
||||
return new URL('http://feedback-link.com/?score=' + encodeURIComponent(score) + '&uuid=' + encodeURIComponent(_uuid));
|
||||
}
|
||||
},
|
||||
urlUtils: {
|
||||
urlFor: (type) => {
|
||||
if (type === 'image') {
|
||||
return 'http://icon.example.com';
|
||||
}
|
||||
return 'http://example.com/subdirectory';
|
||||
},
|
||||
isSiteUrl: (u) => {
|
||||
return u.hostname === 'example.com';
|
||||
}
|
||||
},
|
||||
settingsCache: {
|
||||
get: (key) => {
|
||||
return settings[key];
|
||||
}
|
||||
},
|
||||
getPostUrl: () => {
|
||||
return 'http://example.com';
|
||||
}
|
||||
});
|
||||
|
||||
beforeEach(function () {
|
||||
settings = {};
|
||||
});
|
||||
|
||||
it('uses default accent color', async function () {
|
||||
const html = '';
|
||||
const post = createModel({
|
||||
posts_meta: createModel({}),
|
||||
loaded: ['posts_meta']
|
||||
});
|
||||
const newsletter = createModel({});
|
||||
const data = await emailRenderer.getTemplateData({post, newsletter, html, addPaywall: false});
|
||||
assert.equal(data.accentColor, '#15212A');
|
||||
});
|
||||
|
||||
it('handles invalid accent color', async function () {
|
||||
const html = '';
|
||||
settings.accent_color = '#QR';
|
||||
const post = createModel({
|
||||
posts_meta: createModel({}),
|
||||
loaded: ['posts_meta']
|
||||
});
|
||||
const newsletter = createModel({});
|
||||
const data = await emailRenderer.getTemplateData({post, newsletter, html, addPaywall: false});
|
||||
assert.equal(data.accentColor, '#15212A');
|
||||
});
|
||||
|
||||
it('uses post published_at', async function () {
|
||||
const html = '';
|
||||
const post = createModel({
|
||||
posts_meta: createModel({}),
|
||||
loaded: ['posts_meta'],
|
||||
published_at: new Date(0)
|
||||
});
|
||||
const newsletter = createModel({});
|
||||
const data = await emailRenderer.getTemplateData({post, newsletter, html, addPaywall: false});
|
||||
assert.equal(data.post.publishedAt, '1 Jan 1970');
|
||||
});
|
||||
|
||||
it('show feature image if post has feature image', async function () {
|
||||
const html = '';
|
||||
const post = createModel({
|
||||
posts_meta: createModel({}),
|
||||
loaded: ['posts_meta'],
|
||||
published_at: new Date(0),
|
||||
feature_image: 'http://example.com/image.jpg'
|
||||
});
|
||||
const newsletter = createModel({
|
||||
show_feature_image: true
|
||||
});
|
||||
const data = await emailRenderer.getTemplateData({post, newsletter, html, addPaywall: false});
|
||||
assert.equal(data.showFeatureImage, true);
|
||||
});
|
||||
|
||||
it('uses newsletter font styles', async function () {
|
||||
const html = '';
|
||||
const post = createModel({
|
||||
posts_meta: createModel({}),
|
||||
loaded: ['posts_meta'],
|
||||
published_at: new Date(0)
|
||||
});
|
||||
const newsletter = createModel({
|
||||
title_font_category: 'serif',
|
||||
title_alignment: 'left',
|
||||
body_font_category: 'sans_serif'
|
||||
});
|
||||
const data = await emailRenderer.getTemplateData({post, newsletter, html, addPaywall: false});
|
||||
assert.deepEqual(data.classes, {
|
||||
title: 'post-title post-title-serif post-title-left',
|
||||
titleLink: 'post-title-link post-title-link-left',
|
||||
meta: 'post-meta post-meta-left',
|
||||
body: 'post-content-sans-serif'
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('createUnsubscribeUrl', function () {
|
||||
it('includes member uuid and newsletter id', async function () {
|
||||
const emailRenderer = new EmailRenderer({
|
||||
urlUtils: {
|
||||
urlFor() {
|
||||
return 'http://example.com/subdirectory';
|
||||
}
|
||||
}
|
||||
});
|
||||
const response = await emailRenderer.createUnsubscribeUrl('memberuuid', {
|
||||
newsletterUuid: 'newsletteruuid'
|
||||
});
|
||||
assert.equal(response, `http://example.com/subdirectory/unsubscribe/?uuid=memberuuid&newsletter=newsletteruuid`);
|
||||
});
|
||||
|
||||
it('includes comments', async function () {
|
||||
const emailRenderer = new EmailRenderer({
|
||||
urlUtils: {
|
||||
urlFor() {
|
||||
return 'http://example.com/subdirectory';
|
||||
}
|
||||
}
|
||||
});
|
||||
const response = await emailRenderer.createUnsubscribeUrl('memberuuid', {
|
||||
comments: true
|
||||
});
|
||||
assert.equal(response, `http://example.com/subdirectory/unsubscribe/?uuid=memberuuid&comments=1`);
|
||||
});
|
||||
|
||||
it('works for previews', async function () {
|
||||
const emailRenderer = new EmailRenderer({
|
||||
urlUtils: {
|
||||
urlFor() {
|
||||
return 'http://example.com/subdirectory';
|
||||
}
|
||||
}
|
||||
});
|
||||
const response = await emailRenderer.createUnsubscribeUrl();
|
||||
assert.equal(response, `http://example.com/subdirectory/unsubscribe/?preview=1`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('limitImageWidth', function () {
|
||||
it('Limits width of local images', async function () {
|
||||
const emailRenderer = new EmailRenderer({
|
||||
@ -550,6 +953,25 @@ describe('Email renderer', function () {
|
||||
assert.equal(response.href, 'http://your-blog.com/content/images/size/w1200/2017/01/02/example.png');
|
||||
});
|
||||
|
||||
it('Ignores and logs errors', async function () {
|
||||
const emailRenderer = new EmailRenderer({
|
||||
imageSize: {
|
||||
getImageSizeFromUrl() {
|
||||
throw new Error('Oops, this is a test.');
|
||||
}
|
||||
},
|
||||
storageUtils: {
|
||||
isLocalImage(url) {
|
||||
return url === 'http://your-blog.com/content/images/2017/01/02/example.png';
|
||||
}
|
||||
}
|
||||
});
|
||||
const response = await emailRenderer.limitImageWidth('http://your-blog.com/content/images/2017/01/02/example.png');
|
||||
assert.equal(response.width, 0);
|
||||
assert.equal(response.href, 'http://your-blog.com/content/images/2017/01/02/example.png');
|
||||
sinon.assert.calledOnce(logStub);
|
||||
});
|
||||
|
||||
it('Limits width of unsplash images', async function () {
|
||||
const emailRenderer = new EmailRenderer({
|
||||
imageSize: {
|
||||
|
@ -6,8 +6,26 @@ const createModel = (propertiesAndRelations) => {
|
||||
return {
|
||||
id,
|
||||
getLazyRelation: (relation) => {
|
||||
propertiesAndRelations.loaded = propertiesAndRelations.loaded ?? [];
|
||||
if (!propertiesAndRelations.loaded.includes(relation)) {
|
||||
propertiesAndRelations.loaded.push(relation);
|
||||
}
|
||||
if (Array.isArray(propertiesAndRelations[relation])) {
|
||||
return Promise.resolve({
|
||||
models: propertiesAndRelations[relation]
|
||||
});
|
||||
}
|
||||
return Promise.resolve(propertiesAndRelations[relation]);
|
||||
},
|
||||
related: (relation) => {
|
||||
if (!Object.keys(propertiesAndRelations).includes('loaded')) {
|
||||
throw new Error(`Model.related('${relation}'): When creating a test model via createModel you must include 'loaded' to specify which relations are already loaded and useable via Model.related.`);
|
||||
}
|
||||
if (!propertiesAndRelations.loaded.includes(relation)) {
|
||||
throw new Error(`Model.related('${relation}') was used on a test model that didn't explicitly loaded that relation.`);
|
||||
}
|
||||
return propertiesAndRelations[relation];
|
||||
},
|
||||
get: (property) => {
|
||||
return propertiesAndRelations[property];
|
||||
},
|
||||
|
Loading…
Reference in New Issue
Block a user