Ghost/ghost/email-service/test/email-renderer.test.js
Chris Raible 64c9e66b56
🐛 Fixed broken link tracking in newsletters (#16473)
refs https://github.com/TryGhost/Team/issues/2805

When we render mobiledoc to HTML, it automatically escapes HTML entities in the process, so a button or directly pasted link with href="https://example.com?code=test" will be rendered as href="https://example.com?code&#x3Dtest" as the url is encoded in the rendered HTML. Our link tracking was using the encoded URL as the redirect URL in newsletters, causing certain links to break.

This change updates the link tracking to decode the URL with `entities.decode(url)` so we store the correct redirect URL in our DB and ensure link tracking redirects to the correct url from newsletters.

---------

Co-authored-by: Rishabh <zrishabhgarg@gmail.com>
2023-03-24 18:44:55 +05:30

1949 lines
77 KiB
JavaScript

const {EmailRenderer} = require('../');
const assert = require('assert');
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>';
let postUrl = 'http://example.com';
let customSettings = {};
let emailRenderer;
let basePost;
let addTrackingToUrlStub;
beforeEach(function () {
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: () => true
}
});
});
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('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=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=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.'
}
]
})
}
});
});
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');
});
});
});