Ghost/ghost/email-service/test/email-renderer.test.js
Hannah Wolfe 6161f94910
Updated to use assert/strict everywhere (#17047)
refs: https://github.com/TryGhost/Toolbox/issues/595

We're rolling out new rules around the node assert library, the first of which is enforcing the use of assert/strict. This means we don't need to use the strict version of methods, as the standard version will work that way by default.

This caught some gotchas in our existing usage of assert where the lack of strict mode had unexpected results:
- Url matching needs to be done on `url.href` see aa58b354a4
- Null and undefined are not the same thing,  there were a few cases of this being confused
- Particularly questionable changes in [PostExporter tests](c1a468744b) tracked [here](https://github.com/TryGhost/Team/issues/3505).
- A typo see eaac9c293a

Moving forward, using assert strict should help us to catch unexpected behaviour, particularly around nulls and undefineds during implementation.
2023-06-21 09:56:59 +01:00

2119 lines
83 KiB
JavaScript

const {EmailRenderer} = require('../');
const assert = require('assert/strict');
const cheerio = require('cheerio');
const {createModel, createModelClass} = require('./utils');
const linkReplacer = require('@tryghost/link-replacer');
const sinon = require('sinon');
const logging = require('@tryghost/logging');
const {HtmlValidate} = require('html-validate');
function validateHtml(html) {
const htmlvalidate = new HtmlValidate({
extends: [
'html-validate:document',
'html-validate:standard'
],
rules: {
// We need deprecated attrs for legacy tables in older email clients
'no-deprecated-attr': 'off',
// Don't care that the first <hx> isn't <h1>
'heading-level': 'off'
},
elements: [
'html5',
// By default, html-validate requires the 'lang' attribute on the <html> tag. We don't really want that for now.
{
html: {
attributes: {
lang: {
required: false
}
}
}
}
]
});
const report = htmlvalidate.validateString(html);
// Improve debugging and show a snippet of the invalid HTML instead of just the line number or a huge HTML-dump
const parsedErrors = [];
if (!report.valid) {
const lines = html.split('\n');
const messages = report.results[0].messages;
for (const item of messages) {
if (item.severity !== 2) {
// Ignore warnings
continue;
}
const start = Math.max(item.line - 4, 0);
const end = Math.min(item.line + 4, lines.length - 1);
const _html = lines.slice(start, end).map(l => l.trim()).join('\n');
parsedErrors.push(`${item.ruleId}: ${item.message}\n At line ${item.line}, col ${item.column}\n HTML-snippet:\n${_html}`);
}
}
// Fail if invalid HTML
assert.equal(report.valid, true, 'Expected valid HTML without warnings, got errors:\n' + parsedErrors.join('\n\n'));
}
describe('Email renderer', function () {
let logStub;
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/'
},
labs: {
isSet: () => true
},
settingsCache: {
get: (key) => {
if (key === 'timezone') {
return 'UTC';
}
}
}
});
newsletter = createModel({
uuid: 'newsletteruuid'
});
member = {
id: '456',
uuid: 'myuuid',
name: 'Test User',
email: 'test@example.com',
createdAt: new Date(2023, 2, 13, 12, 0),
status: 'free'
};
});
it('returns an empty list of replacements if nothing is used', function () {
const html = 'Hello world';
const replacements = emailRenderer.buildReplacementDefinitions({html, newsletterUuid: newsletter.get('uuid')});
assert.equal(replacements.length, 0);
});
it('returns a replacement if it is used', function () {
const html = 'Hello world %%{uuid}%%';
const replacements = emailRenderer.buildReplacementDefinitions({html, newsletterUuid: newsletter.get('uuid')});
assert.equal(replacements.length, 1);
assert.equal(replacements[0].token.toString(), '/%%\\{uuid\\}%%/g');
assert.equal(replacements[0].id, 'uuid');
assert.equal(replacements[0].getValue(member), 'myuuid');
});
it('returns a replacement only once if used multiple times', function () {
const html = 'Hello world %%{uuid}%% And %%{uuid}%%';
const replacements = emailRenderer.buildReplacementDefinitions({html, newsletterUuid: newsletter.get('uuid')});
assert.equal(replacements.length, 1);
assert.equal(replacements[0].token.toString(), '/%%\\{uuid\\}%%/g');
assert.equal(replacements[0].id, 'uuid');
assert.equal(replacements[0].getValue(member), 'myuuid');
});
it('returns correct first name', function () {
const html = 'Hello %%{first_name}%%,';
const replacements = emailRenderer.buildReplacementDefinitions({html, newsletterUuid: newsletter.get('uuid')});
assert.equal(replacements.length, 1);
assert.equal(replacements[0].token.toString(), '/%%\\{first_name\\}%%/g');
assert.equal(replacements[0].id, 'first_name');
assert.equal(replacements[0].getValue(member), 'Test');
});
it('returns correct unsubscribe url', function () {
const html = 'Hello %%{unsubscribe_url}%%,';
const replacements = emailRenderer.buildReplacementDefinitions({html, newsletterUuid: newsletter.get('uuid')});
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('returns correct name', function () {
const html = 'Hello %%{name}%%,';
const replacements = emailRenderer.buildReplacementDefinitions({html, newsletterUuid: newsletter.get('uuid')});
assert.equal(replacements.length, 1);
assert.equal(replacements[0].token.toString(), '/%%\\{name\\}%%/g');
assert.equal(replacements[0].id, 'name');
assert.equal(replacements[0].getValue(member), 'Test User');
});
it('returns hidden class for missing name', function () {
member.name = '';
const html = 'Hello %%{name_class}%%,';
const replacements = emailRenderer.buildReplacementDefinitions({html, newsletterUuid: newsletter.get('uuid')});
assert.equal(replacements.length, 1);
assert.equal(replacements[0].token.toString(), '/%%\\{name_class\\}%%/g');
assert.equal(replacements[0].id, 'name_class');
assert.equal(replacements[0].getValue(member), 'hidden');
});
it('returns empty class for available name', function () {
const html = 'Hello %%{name_class}%%,';
const replacements = emailRenderer.buildReplacementDefinitions({html, newsletterUuid: newsletter.get('uuid')});
assert.equal(replacements.length, 1);
assert.equal(replacements[0].token.toString(), '/%%\\{name_class\\}%%/g');
assert.equal(replacements[0].id, 'name_class');
assert.equal(replacements[0].getValue(member), '');
});
it('returns correct email', function () {
const html = 'Hello %%{email}%%,';
const replacements = emailRenderer.buildReplacementDefinitions({html, newsletterUuid: newsletter.get('uuid')});
assert.equal(replacements.length, 1);
assert.equal(replacements[0].token.toString(), '/%%\\{email\\}%%/g');
assert.equal(replacements[0].id, 'email');
assert.equal(replacements[0].getValue(member), 'test@example.com');
});
it('returns correct status', function () {
const html = 'Hello %%{status}%%,';
const replacements = emailRenderer.buildReplacementDefinitions({html, newsletterUuid: newsletter.get('uuid')});
assert.equal(replacements.length, 1);
assert.equal(replacements[0].token.toString(), '/%%\\{status\\}%%/g');
assert.equal(replacements[0].id, 'status');
assert.equal(replacements[0].getValue(member), 'free');
});
it('returns mapped complimentary status', function () {
member.status = 'comped';
const html = 'Hello %%{status}%%,';
const replacements = emailRenderer.buildReplacementDefinitions({html, newsletterUuid: newsletter.get('uuid')});
assert.equal(replacements.length, 1);
assert.equal(replacements[0].token.toString(), '/%%\\{status\\}%%/g');
assert.equal(replacements[0].id, 'status');
assert.equal(replacements[0].getValue(member), 'complimentary');
});
it('returns mapped trialing status', function () {
member.status = 'paid';
member.subscriptions = [
{
status: 'trialing',
trial_end_at: new Date(2050, 2, 13, 12, 0),
current_period_end: new Date(2023, 2, 13, 12, 0),
cancel_at_period_end: false
}
];
const html = 'Hello %%{status}%%,';
const replacements = emailRenderer.buildReplacementDefinitions({html, newsletterUuid: newsletter.get('uuid')});
assert.equal(replacements.length, 1);
assert.equal(replacements[0].token.toString(), '/%%\\{status\\}%%/g');
assert.equal(replacements[0].id, 'status');
assert.equal(replacements[0].getValue(member), 'trialing');
});
it('returns manage_account_url', function () {
const html = 'Hello %%{manage_account_url}%%,';
const replacements = emailRenderer.buildReplacementDefinitions({html, newsletterUuid: newsletter.get('uuid')});
assert.equal(replacements.length, 1);
assert.equal(replacements[0].token.toString(), '/%%\\{manage_account_url\\}%%/g');
assert.equal(replacements[0].id, 'manage_account_url');
assert.equal(replacements[0].getValue(member), 'http://example.com/subdirectory/#/portal/account');
});
it('returns status_text', function () {
const html = 'Hello %%{status_text}%%,';
member.status = 'paid';
member.subscriptions = [
{
status: 'trialing',
trial_end_at: new Date(2050, 2, 13, 12, 0),
current_period_end: new Date(2023, 2, 13, 12, 0),
cancel_at_period_end: false
}
];
const replacements = emailRenderer.buildReplacementDefinitions({html, newsletterUuid: newsletter.get('uuid')});
assert.equal(replacements.length, 1);
assert.equal(replacements[0].token.toString(), '/%%\\{status_text\\}%%/g');
assert.equal(replacements[0].id, 'status_text');
assert.equal(replacements[0].getValue(member), 'Your free trial ends on 13 March 2050, at which time you will be charged the regular price. You can always cancel before then.');
});
it('returns correct createdAt', function () {
const html = 'Hello %%{created_at}%%,';
const replacements = emailRenderer.buildReplacementDefinitions({html, newsletterUuid: newsletter.get('uuid')});
assert.equal(replacements.length, 1);
assert.equal(replacements[0].token.toString(), '/%%\\{created_at\\}%%/g');
assert.equal(replacements[0].id, 'created_at');
assert.equal(replacements[0].getValue(member), '13 March 2023');
});
it('returns missing created at', function () {
member.createdAt = null;
const html = 'Hello %%{created_at}%%,';
const replacements = emailRenderer.buildReplacementDefinitions({html, newsletterUuid: newsletter.get('uuid')});
assert.equal(replacements.length, 1);
assert.equal(replacements[0].token.toString(), '/%%\\{created_at\\}%%/g');
assert.equal(replacements[0].id, 'created_at');
assert.equal(replacements[0].getValue(member), '');
});
it('supports fallback values', function () {
const html = 'Hey %%{first_name, "there"}%%,';
const replacements = emailRenderer.buildReplacementDefinitions({html, newsletterUuid: newsletter.get('uuid')});
assert.equal(replacements.length, 1);
assert.equal(replacements[0].token.toString(), '/%%\\{first_name, (?:"|&quot;)there(?:"|&quot;)\\}%%/g');
assert.equal(replacements[0].id, 'first_name_2');
assert.equal(replacements[0].getValue(member), 'Test');
// In case of empty name
assert.equal(replacements[0].getValue({name: ''}), 'there');
});
it('supports combination of multiple fallback values', function () {
const html = 'Hey %%{first_name, "there"}%%, %%{first_name, "member"}%% %%{first_name}%% %%{first_name, "there"}%%';
const replacements = emailRenderer.buildReplacementDefinitions({html, newsletterUuid: newsletter.get('uuid')});
assert.equal(replacements.length, 3);
assert.equal(replacements[0].token.toString(), '/%%\\{first_name, (?:"|&quot;)there(?:"|&quot;)\\}%%/g');
assert.equal(replacements[0].id, 'first_name_2');
assert.equal(replacements[0].getValue(member), 'Test');
// In case of empty name
assert.equal(replacements[0].getValue({name: ''}), 'there');
assert.equal(replacements[1].token.toString(), '/%%\\{first_name, (?:"|&quot;)member(?:"|&quot;)\\}%%/g');
assert.equal(replacements[1].id, 'first_name_3');
assert.equal(replacements[1].getValue(member), 'Test');
// In case of empty name
assert.equal(replacements[1].getValue({name: ''}), 'member');
assert.equal(replacements[2].token.toString(), '/%%\\{first_name\\}%%/g');
assert.equal(replacements[2].id, 'first_name');
assert.equal(replacements[2].getValue(member), 'Test');
// In case of empty name
assert.equal(replacements[2].getValue({name: ''}), '');
});
});
describe('isMemberTrialing', function () {
let emailRenderer;
beforeEach(function () {
emailRenderer = new EmailRenderer({
urlUtils: {
urlFor: () => 'http://example.com/subdirectory/'
},
labs: {
isSet: () => true
},
settingsCache: {
get: (key) => {
if (key === 'timezone') {
return 'UTC';
}
}
}
});
});
it('Returns false for free member', function () {
const member = {
id: '456',
uuid: 'myuuid',
name: 'Test User',
email: 'test@example.com',
createdAt: new Date(2023, 2, 13, 12, 0),
status: 'free'
};
const result = emailRenderer.isMemberTrialing(member);
assert.equal(result, false);
});
it('Returns false for paid member without trial', function () {
const member = {
id: '456',
uuid: 'myuuid',
name: 'Test User',
email: 'test@example.com',
createdAt: new Date(2023, 2, 13, 12, 0),
status: 'paid',
subscriptions: [
{
status: 'active',
current_period_end: new Date(2023, 2, 13, 12, 0),
cancel_at_period_end: false
}
]
};
const result = emailRenderer.isMemberTrialing(member);
assert.equal(result, false);
});
it('Returns true for trialing paid member', function () {
const member = {
id: '456',
uuid: 'myuuid',
name: 'Test User',
email: 'test@example.com',
createdAt: new Date(2023, 2, 13, 12, 0),
status: 'paid',
subscriptions: [
{
status: 'trialing',
trial_end_at: new Date(2050, 2, 13, 12, 0),
current_period_end: new Date(2023, 2, 13, 12, 0),
cancel_at_period_end: false
}
],
tiers: []
};
const result = emailRenderer.isMemberTrialing(member);
assert.equal(result, true);
});
it('Returns false for expired trialing paid member', function () {
const member = {
id: '456',
uuid: 'myuuid',
name: 'Test User',
email: 'test@example.com',
createdAt: new Date(2023, 2, 13, 12, 0),
status: 'paid',
subscriptions: [
{
status: 'trialing',
trial_end_at: new Date(2000, 2, 13, 12, 0),
current_period_end: new Date(2023, 2, 13, 12, 0),
cancel_at_period_end: false
}
],
tiers: []
};
const result = emailRenderer.isMemberTrialing(member);
assert.equal(result, false);
});
});
describe('getMemberStatusText', function () {
let emailRenderer;
beforeEach(function () {
emailRenderer = new EmailRenderer({
urlUtils: {
urlFor: () => 'http://example.com/subdirectory/'
},
labs: {
isSet: () => true
},
settingsCache: {
get: (key) => {
if (key === 'timezone') {
return 'UTC';
}
}
}
});
});
it('Returns for free member', function () {
const member = {
id: '456',
uuid: 'myuuid',
name: 'Test User',
email: 'test@example.com',
createdAt: new Date(2023, 2, 13, 12, 0),
status: 'free'
};
const result = emailRenderer.getMemberStatusText(member);
assert.equal(result, '');
});
it('Returns for active paid member', function () {
const member = {
id: '456',
uuid: 'myuuid',
name: 'Test User',
email: 'test@example.com',
createdAt: new Date(2023, 2, 13, 12, 0),
status: 'paid',
subscriptions: [
{
status: 'active',
current_period_end: new Date(2023, 2, 13, 12, 0),
cancel_at_period_end: false
}
]
};
const result = emailRenderer.getMemberStatusText(member);
assert.equal(result, 'Your subscription will renew on 13 March 2023.');
});
it('Returns for canceled paid member', function () {
const member = {
id: '456',
uuid: 'myuuid',
name: 'Test User',
email: 'test@example.com',
createdAt: new Date(2023, 2, 13, 12, 0),
status: 'paid',
subscriptions: [
{
status: 'active',
current_period_end: new Date(2023, 2, 13, 12, 0),
cancel_at_period_end: true
}
]
};
const result = emailRenderer.getMemberStatusText(member);
assert.equal(result, 'Your subscription has been canceled and will expire on 13 March 2023. You can resume your subscription via your account settings.');
});
it('Returns for expired paid member', function () {
const member = {
id: '456',
uuid: 'myuuid',
name: 'Test User',
email: 'test@example.com',
createdAt: new Date(2023, 2, 13, 12, 0),
status: 'paid',
subscriptions: [
{
status: 'canceled',
current_period_end: new Date(2023, 2, 13, 12, 0),
cancel_at_period_end: true
}
],
tiers: []
};
const result = emailRenderer.getMemberStatusText(member);
assert.equal(result, 'Your subscription has expired.');
});
it('Returns for trialing paid member', function () {
const member = {
id: '456',
uuid: 'myuuid',
name: 'Test User',
email: 'test@example.com',
createdAt: new Date(2023, 2, 13, 12, 0),
status: 'paid',
subscriptions: [
{
status: 'trialing',
trial_end_at: new Date(2050, 2, 13, 12, 0),
current_period_end: new Date(2023, 2, 13, 12, 0),
cancel_at_period_end: false
}
],
tiers: []
};
const result = emailRenderer.getMemberStatusText(member);
assert.equal(result, 'Your free trial ends on 13 March 2050, at which time you will be charged the regular price. You can always cancel before then.');
});
it('Returns for infinite complimentary member', function () {
const member = {
id: '456',
uuid: 'myuuid',
name: 'Test User',
email: 'test@example.com',
createdAt: new Date(2023, 2, 13, 12, 0),
status: 'comped',
subscriptions: [],
tiers: [
{
name: 'Silver',
expiry_at: null
}
]
};
const result = emailRenderer.getMemberStatusText(member);
assert.equal(result, '');
});
it('Returns for expiring complimentary member', function () {
const member = {
id: '456',
uuid: 'myuuid',
name: 'Test User',
email: 'test@example.com',
createdAt: new Date(2023, 2, 13, 12, 0),
status: 'comped',
subscriptions: [],
tiers: [
{
name: 'Silver',
expiry_at: new Date(2050, 2, 13, 12, 0)
}
]
};
const result = emailRenderer.getMemberStatusText(member);
assert.equal(result, 'Your subscription will expire on 13 March 2050.');
});
it('Returns for a paid member without subscriptions', function () {
const member = {
id: '456',
uuid: 'myuuid',
name: 'Test User',
email: 'test@example.com',
createdAt: new Date(2023, 2, 13, 12, 0),
status: 'paid',
subscriptions: [],
tiers: [
{
name: 'Silver',
expiry_at: new Date(2050, 2, 13, 12, 0)
}
]
};
const result = emailRenderer.getMemberStatusText(member);
assert.equal(result, 'Your subscription has been canceled and will expire on 13 March 2050. You can resume your subscription via your account settings.');
});
it('Returns for an infinte paid member without subscriptions', function () {
const member = {
id: '456',
uuid: 'myuuid',
name: 'Test User',
email: 'test@example.com',
createdAt: new Date(2023, 2, 13, 12, 0),
status: 'paid',
subscriptions: [],
tiers: [
{
name: 'Silver',
expiry_at: null
}
]
};
const result = emailRenderer.getMemberStatusText(member);
assert.equal(result, '');
});
});
describe('getSubject', function () {
const emailRenderer = new EmailRenderer({
urlUtils: {
urlFor: () => 'http://example.com'
},
labs: {
isSet: () => true
}
});
it('returns a post with correct subject from meta', function () {
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 () {
const post = createModel({
posts_meta: createModel({
email_subject: ''
}),
title: 'Sample Post',
loaded: ['posts_meta']
});
let response = emailRenderer.getSubject(post);
response.should.equal('Sample Post');
});
});
describe('getFromAddress', function () {
let siteTitle = 'Test Blog';
let emailRenderer = new EmailRenderer({
settingsCache: {
get: (key) => {
if (key === 'title') {
return siteTitle;
}
}
},
settingsHelpers: {
getNoReplyAddress: () => {
return 'reply@example.com';
}
},
labs: {
isSet: () => true
}
});
it('returns correct from address for newsletter', function () {
const newsletter = createModel({
sender_email: 'ghost@example.com',
sender_name: 'Ghost'
});
const response = emailRenderer.getFromAddress({}, newsletter);
response.should.equal('"Ghost" <ghost@example.com>');
});
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 () {
let emailRenderer = new EmailRenderer({
settingsCache: {
get: (key) => {
if (key === 'title') {
return 'Test Blog';
}
}
},
settingsHelpers: {
getMembersSupportAddress: () => {
return 'support@example.com';
},
getNoReplyAddress: () => {
return 'reply@example.com';
}
},
labs: {
isSet: () => true
}
});
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 () {
let emailRenderer = new EmailRenderer({
renderers: {
lexical: {
render: () => {
return '<p> Lexical Test</p>';
}
},
mobiledoc: {
render: () => {
return '<p> Mobiledoc Test</p>';
}
}
},
getPostUrl: () => {
return 'http://example.com/post-id';
},
labs: {
isSet: () => true
}
});
it('returns correct empty segment for post', function () {
let post = {
get: (key) => {
if (key === 'lexical') {
return '{}';
}
}
};
let response = emailRenderer.getSegments(post);
response.should.eql([null]);
post = {
get: (key) => {
if (key === 'mobiledoc') {
return '{}';
}
}
};
response = emailRenderer.getSegments(post);
response.should.eql([null]);
});
it('returns correct segments for post with members only card', function () {
emailRenderer = new EmailRenderer({
renderers: {
lexical: {
render: () => {
return '<p> Lexical Test <!--members-only--> members only section</p>';
}
}
},
getPostUrl: () => {
return 'http://example.com/post-id';
},
labs: {
isSet: () => true
}
});
let post = {
get: (key) => {
if (key === 'lexical') {
return '{}';
}
}
};
let response = emailRenderer.getSegments(post);
response.should.eql(['status:free', 'status:-free']);
});
it('returns correct segments for post with email card', function () {
emailRenderer = new EmailRenderer({
renderers: {
lexical: {
render: () => {
return '<html> <div> Lexical Test </div> <div data-gh-segment="status:-free"> members only section</div> </html>';
}
}
},
getPostUrl: () => {
return 'http://example.com/post-id';
},
labs: {
isSet: () => true
}
});
let post = {
get: (key) => {
if (key === 'lexical') {
return '{}';
}
}
};
let response = emailRenderer.getSegments(post);
response.should.eql(['status:free', 'status:-free']);
});
});
describe('renderBody', function () {
let renderedPost = '<p>Lexical Test</p><img class="is-light-background" src="test-dark" /><img class="is-dark-background" src="test-light" />';
let postUrl = 'http://example.com';
let customSettings = {};
let emailRenderer;
let basePost;
let addTrackingToUrlStub;
let labsEnabled;
beforeEach(function () {
labsEnabled = true;
basePost = {
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']
};
postUrl = 'http://example.com';
customSettings = {};
addTrackingToUrlStub = sinon.stub();
addTrackingToUrlStub.callsFake((u, _post, uuid) => {
return new URL('http://tracked-link.com/?m=' + encodeURIComponent(uuid) + '&url=' + encodeURIComponent(u.href));
});
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) => {
if (customSettings[key]) {
return customSettings[key];
}
if (key === 'accent_color') {
return '#ffffff';
}
if (key === 'timezone') {
return 'Etc/UTC';
}
if (key === 'title') {
return 'Test Blog';
}
if (key === 'icon') {
return 'ICON';
}
}
},
getPostUrl: () => {
return postUrl;
},
renderers: {
lexical: {
render: () => {
return renderedPost;
}
},
mobiledoc: {
render: () => {
return '<p> Mobiledoc Test</p>';
}
}
},
linkReplacer,
memberAttributionService: {
addPostAttributionTracking: (u) => {
u.searchParams.append('post_tracking', 'added');
return u;
}
},
linkTracking: {
service: {
addTrackingToUrl: addTrackingToUrlStub
}
},
outboundLinkTagger: {
addToUrl: (u, newsletter) => {
u.searchParams.append('source_tracking', newsletter?.get('name') ?? 'site');
return u;
}
},
labs: {
isSet: () => labsEnabled
}
});
});
it('Renders with labs disabled', async function () {
labsEnabled = false;
const post = createModel(basePost);
const newsletter = createModel({
header_image: null,
name: 'Test Newsletter',
show_badge: false,
feedback_enabled: true,
show_post_title_section: true
});
const segment = null;
const options = {};
await emailRenderer.renderBody(
post,
newsletter,
segment,
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,
show_post_title_section: true
});
const segment = null;
const options = {};
let response = await emailRenderer.renderBody(
post,
newsletter,
segment,
options
);
const $ = cheerio.load(response.html);
response.plaintext.should.containEql('Test Post');
// Unsubscribe button included
response.plaintext.should.containEql('Unsubscribe [%%{unsubscribe_url}%%]');
response.html.should.containEql('Unsubscribe');
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,
show_post_title_section: 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,
show_post_title_section: true
});
const segment = null;
const options = {};
let response = await emailRenderer.renderBody(
post,
newsletter,
segment,
options
);
assert.match(response.html, /By A &amp; 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,
show_post_title_section: 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,
show_post_title_section: 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('works in dark mode', async function () {
const post = createModel(basePost);
const newsletter = createModel({
header_image: null,
name: 'Test Newsletter',
show_badge: false,
feedback_enabled: true,
show_post_title_section: true,
background_color: '#000000'
});
const segment = null;
const options = {};
let response = await emailRenderer.renderBody(
post,
newsletter,
segment,
options
);
assert.doesNotMatch(response.html, /is-light-background/);
});
it('works in light mode', async function () {
const post = createModel(basePost);
const newsletter = createModel({
header_image: null,
name: 'Test Newsletter',
show_badge: false,
feedback_enabled: true,
show_post_title_section: true,
background_color: '#FFFFFF'
});
const segment = null;
const options = {};
let response = await emailRenderer.renderBody(
post,
newsletter,
segment,
options
);
assert.doesNotMatch(response.html, /is-dark-background/);
});
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,
show_post_title_section: 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://encoded-link.com?code&#x3D;test">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=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%2Fencoded-link.com%2F%3Fcode%3Dtest%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}%%`,
`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('handles encoded links', async function () {
const post = createModel(basePost);
const newsletter = createModel({
header_image: null,
name: 'Test Newsletter',
show_badge: true,
feedback_enabled: true,
show_post_title_section: 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=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}%%`,
`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 () {
renderedPost = '<div> Lexical Test </div> <div data-gh-segment="status:-free"> members only section</div> some text for both <!--members-only--> finishing part only for members';
let post = {
related: () => {
return null;
},
get: (key) => {
if (key === 'lexical') {
return '{}';
}
if (key === 'visibility') {
return 'paid';
}
if (key === 'title') {
return 'Test Post';
}
},
getLazyRelation: () => {
return {
models: [{
get: (key) => {
if (key === 'name') {
return 'Test Author';
}
}
}]
};
}
};
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 options = {};
let response = await emailRenderer.renderBody(
post,
newsletter,
'status:free',
options
);
response.plaintext.should.containEql('Test Post');
response.plaintext.should.containEql('Unsubscribe [%%{unsubscribe_url}%%]');
response.plaintext.should.containEql('http://example.com');
response.html.should.containEql('Test Post');
response.html.should.containEql('Unsubscribe');
response.html.should.containEql('http://example.com');
response.replacements.length.should.eql(2);
response.replacements.should.match([
{
id: 'uuid'
},
{
id: 'unsubscribe_url',
token: /%%\{unsubscribe_url\}%%/g
}
]);
response.html.should.not.containEql('members only section');
response.html.should.containEql('some text for both');
response.html.should.not.containEql('finishing part only for members');
response.html.should.containEql('Become a paid member of Test Blog to get access to all');
let responsePaid = await emailRenderer.renderBody(
post,
newsletter,
'status:-free',
options
);
responsePaid.html.should.containEql('members only section');
responsePaid.html.should.containEql('some text for both');
responsePaid.html.should.containEql('finishing part only for members');
responsePaid.html.should.not.containEql('Become a paid member of Test Blog to get access to all');
});
it('should output valid HTML and escape HTML characters in mobiledoc', async function () {
const post = createModel({
...basePost,
title: 'This is\' a blog po"st test <3</body>',
excerpt: 'This is a blog post test <3</body>',
authors: [
createModel({
name: 'This is a blog post test <3</body>'
})
],
posts_meta: createModel({
feature_image_alt: 'This is a blog post test <3</body>',
feature_image_caption: 'This is escaped in the frontend'
})
});
postUrl = 'https://testpost.com/t&es<3t-post"</body>/';
customSettings = {
icon: 'icon2<3</body>'
};
const newsletter = createModel({
feedback_enabled: true,
name: 'My newsletter <3</body>',
header_image: 'https://testpost.com/test-post</body>/',
show_header_icon: true,
show_header_title: true,
show_feature_image: true,
title_font_category: 'sans-serif',
title_alignment: 'center',
body_font_category: 'serif',
show_badge: true,
show_header_name: true,
// Note: we don't need to check the footer content because this should contain valid HTML (not text)
footer_content: '<span>Footer content with valid HTML</span>'
});
const segment = null;
const options = {};
const response = await emailRenderer.renderBody(
post,
newsletter,
segment,
options
);
validateHtml(response.html);
// Check footer content is not escaped
assert.equal(response.html.includes('<span>Footer content with valid HTML</span>'), true, 'Should include footer content without escaping');
// Check doesn't contain the non escaped string '<3'
assert.equal(response.html.includes('<3'), false, 'Should escape HTML characters');
});
});
describe('getTemplateData', function () {
let settings = {};
let labsEnabled = true;
let emailRenderer;
beforeEach(function () {
settings = {};
labsEnabled = true;
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';
},
labs: {
isSet: () => labsEnabled
},
models: {
Post: createModelClass({
findAll: [
{
title: 'Test Post 1',
published_at: new Date('2018-01-01T00:00:00.000Z'),
custom_excerpt: 'Super long custom excerpt. Super long custom excerpt. Super long custom excerpt. Super long custom excerpt. Super long custom excerpt.',
feature_image: 'http://example.com/image.jpg'
},
{
title: 'Test Post 2',
published_at: new Date('2018-01-01T00:00:00.000Z'),
feature_image: null,
plaintext: ''
},
{
title: 'Test Post 3',
published_at: null, // required for full test coverage
feature_image: null,
plaintext: 'Nothing special.'
}
]
})
}
});
});
async function templateDataWithSettings(settingsObj) {
const html = '';
const post = createModel({
posts_meta: createModel({}),
loaded: ['posts_meta']
});
const newsletter = createModel({
...settingsObj
});
const data = await emailRenderer.getTemplateData({post, newsletter, html, addPaywall: false});
return data;
}
it('Uses the correct background colors based on settings', async function () {
const tests = [
{input: 'Invalid Color', expected: '#ffffff'},
{input: '#BADA55', expected: '#BADA55'},
{input: 'dark', expected: '#15212a'},
{input: 'light', expected: '#ffffff'},
{input: null, expected: '#ffffff'}
];
for (const test of tests) {
const data = await templateDataWithSettings({
background_color: test.input
});
assert.equal(data.backgroundColor, test.expected);
}
});
it('Uses the correct border colors based on settings', async function () {
settings.accent_color = '#ABC123';
const tests = [
{input: 'Invalid Color', expected: null},
{input: '#BADA55', expected: '#BADA55'},
{input: 'auto', expected: '#FFFFFF', background_color: '#15212A'},
{input: 'auto', expected: '#000000', background_color: '#ffffff'},
{input: 'light', expected: null},
{input: 'accent', expected: settings.accent_color},
{input: 'transparent', expected: null}
];
for (const test of tests) {
const data = await templateDataWithSettings({
border_color: test.input,
background_color: test.background_color
});
assert.equal(data.borderColor, test.expected);
}
});
it('Uses the correct title colors based on settings and background color', async function () {
settings.accent_color = '#DEF456';
const tests = [
{input: '#BADA55', expected: '#BADA55'},
{input: 'accent', expected: settings.accent_color},
{input: 'Invalid Color', expected: '#FFFFFF', background_color: '#15212A'},
{input: null, expected: '#000000', background_color: '#ffffff'}
];
for (const test of tests) {
const data = await templateDataWithSettings({
title_color: test.input,
background_color: test.background_color
});
assert.equal(data.titleColor, test.expected);
}
});
it('Sets the backgroundIsDark correctly', async function () {
const tests = [
{background_color: '#15212A', expected: true},
{background_color: '#ffffff', expected: false}
];
for (const test of tests) {
const data = await templateDataWithSettings({
background_color: test.background_color
});
assert.equal(data.backgroundIsDark, test.expected);
}
});
it('Sets the linkColor correctly', async function () {
settings.accent_color = '#A1B2C3';
const tests = [
{background_color: '#15212A', expected: '#ffffff'},
{background_color: '#ffffff', expected: settings.accent_color}
];
for (const test of tests) {
const data = await templateDataWithSettings({
background_color: test.background_color
});
assert.equal(data.linkColor, test.expected);
}
});
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'
});
});
it('show comment CTA is enabled if labs disabled', async function () {
labsEnabled = false;
settings.comments_enabled = 'all';
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',
show_comment_cta: true
});
const data = await emailRenderer.getTemplateData({post, newsletter, html, addPaywall: false});
assert.equal(data.newsletter.showCommentCta, true);
});
it('show comment CTA is disabled if comments disabled', async function () {
labsEnabled = true;
settings.comments_enabled = 'off';
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',
show_comment_cta: true
});
const data = await emailRenderer.getTemplateData({post, newsletter, html, addPaywall: false});
assert.equal(data.newsletter.showCommentCta, false);
});
it('show comment CTA is disabled if disabled', async function () {
labsEnabled = true;
settings.comments_enabled = 'all';
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',
show_comment_cta: false
});
const data = await emailRenderer.getTemplateData({post, newsletter, html, addPaywall: false});
assert.equal(data.newsletter.showCommentCta, false);
});
it('show comment CTA is enabled if all enabled', async function () {
labsEnabled = true;
settings.comments_enabled = 'all';
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',
show_comment_cta: true
});
const data = await emailRenderer.getTemplateData({post, newsletter, html, addPaywall: false});
assert.equal(data.newsletter.showCommentCta, true);
});
it('showSubscriptionDetails works is enabled', async function () {
labsEnabled = true;
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',
show_subscription_details: true
});
const data = await emailRenderer.getTemplateData({post, newsletter, html, addPaywall: false});
assert.equal(data.newsletter.showSubscriptionDetails, true);
});
it('showSubscriptionDetails can be disabled', async function () {
labsEnabled = true;
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',
show_subscription_details: false
});
const data = await emailRenderer.getTemplateData({post, newsletter, html, addPaywall: false});
assert.equal(data.newsletter.showSubscriptionDetails, false);
});
it('latestPosts can be disabled', async function () {
labsEnabled = true;
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',
show_latest_posts: false
});
const data = await emailRenderer.getTemplateData({post, newsletter, html, addPaywall: false});
assert.deepEqual(data.latestPosts, []);
});
it('latestPosts can be enabled', async function () {
labsEnabled = true;
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',
show_latest_posts: true
});
const data = await emailRenderer.getTemplateData({post, newsletter, html, addPaywall: false});
assert.deepEqual(data.latestPosts,
[
{
excerpt: 'Super long custom excerpt. Super long custom excerpt. Super<span class="mobile-only"> long custom excerpt. Super long custom excer</span>…',
title: 'Test Post 1',
url: 'http://example.com',
featureImage: {
src: 'http://example.com/image.jpg',
width: 0,
height: null
},
featureImageMobile: {
src: 'http://example.com/image.jpg',
width: 0,
height: null
}
},
{
featureImage: null,
featureImageMobile: null,
excerpt: '',
title: 'Test Post 2',
url: 'http://example.com'
},
{
featureImage: null,
featureImageMobile: null,
excerpt: 'Nothing special.',
title: 'Test Post 3',
url: 'http://example.com'
}
]);
});
});
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('truncateText', function () {
it('works for null', async function () {
const emailRenderer = new EmailRenderer({});
assert.equal(emailRenderer.truncateText(null, 100), '');
});
});
describe('truncateHTML', function () {
it('works correctly', async function () {
const emailRenderer = new EmailRenderer({});
assert.equal(emailRenderer.truncateHtml('This is a short one', 5, 10), 'This<span class="mobile-only"> is a</span>…');
assert.equal(emailRenderer.truncateHtml('This is a', 5, 10), 'This<span class="mobile-only"> is a</span><span class="hide-mobile">…</span>');
assert.equal(emailRenderer.truncateHtml('This', 5, 10), 'This');
assert.equal(emailRenderer.truncateHtml('This is a long text', 5, 5), 'This…');
assert.equal(emailRenderer.truncateHtml('This is a long text', 5), 'This…');
assert.equal(emailRenderer.truncateHtml(null, 5, 10), '');
});
});
describe('limitImageWidth', function () {
it('Limits width of local images', async function () {
const emailRenderer = new EmailRenderer({
imageSize: {
getImageSizeFromUrl() {
return {
width: 2000,
height: 1000
};
}
},
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, 600);
assert.equal(response.height, 300);
assert.equal(response.href, 'http://your-blog.com/content/images/size/w1200/2017/01/02/example.png');
});
it('Limits width and height of local images', async function () {
const emailRenderer = new EmailRenderer({
imageSize: {
getImageSizeFromUrl() {
return {
width: 2000,
height: 1000
};
}
},
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', 600, 600);
assert.equal(response.width, 600);
assert.equal(response.height, 600);
assert.equal(response.href, 'http://your-blog.com/content/images/size/w1200h1200/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: {
getImageSizeFromUrl() {
return {
width: 2000
};
}
},
storageUtils: {
isLocalImage(url) {
return url === 'http://your-blog.com/content/images/2017/01/02/example.png';
}
}
});
const response = await emailRenderer.limitImageWidth('https://images.unsplash.com/photo-1657816793628-191deb91e20f?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxMTc3M3wwfDF8YWxsfDJ8fHx8fHwyfHwxNjU3ODkzNjU5&ixlib=rb-1.2.1&q=80&w=2000');
assert.equal(response.width, 600);
assert.equal(response.height, null);
assert.equal(response.href, 'https://images.unsplash.com/photo-1657816793628-191deb91e20f?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxMTc3M3wwfDF8YWxsfDJ8fHx8fHwyfHwxNjU3ODkzNjU5&ixlib=rb-1.2.1&q=80&w=1200');
});
it('Limits width and height of unsplash images', async function () {
const emailRenderer = new EmailRenderer({
imageSize: {
getImageSizeFromUrl() {
return {
width: 2000,
height: 1000
};
}
},
storageUtils: {
isLocalImage(url) {
return url === 'http://your-blog.com/content/images/2017/01/02/example.png';
}
}
});
const response = await emailRenderer.limitImageWidth('https://images.unsplash.com/photo-1657816793628-191deb91e20f?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxMTc3M3wwfDF8YWxsfDJ8fHx8fHwyfHwxNjU3ODkzNjU5&ixlib=rb-1.2.1&q=80&w=2000', 600, 600);
assert.equal(response.width, 600);
assert.equal(response.height, 600);
assert.equal(response.href, 'https://images.unsplash.com/photo-1657816793628-191deb91e20f?crop=entropy&cs=tinysrgb&fit=crop&fm=jpg&ixid=MnwxMTc3M3wwfDF8YWxsfDJ8fHx8fHwyfHwxNjU3ODkzNjU5&ixlib=rb-1.2.1&q=80&w=1200&h=1200');
});
it('Does not increase width of images', async function () {
const emailRenderer = new EmailRenderer({
imageSize: {
getImageSizeFromUrl() {
return {
width: 300
};
}
},
storageUtils: {
isLocalImage(url) {
return url === 'http://your-blog.com/content/images/2017/01/02/example.png';
}
}
});
const response = await emailRenderer.limitImageWidth('https://example.com/image.png');
assert.equal(response.width, 300);
assert.equal(response.href, 'https://example.com/image.png');
});
});
});