2023-01-11 14:13:13 +03:00
const { EmailRenderer } = require ( '../' ) ;
2023-06-21 11:56:59 +03:00
const assert = require ( 'assert/strict' ) ;
2022-12-14 13:24:26 +03:00
const cheerio = require ( 'cheerio' ) ;
2023-03-20 16:30:42 +03:00
const { createModel , createModelClass } = require ( './utils' ) ;
2023-01-11 14:13:13 +03:00
const linkReplacer = require ( '@tryghost/link-replacer' ) ;
const sinon = require ( 'sinon' ) ;
const logging = require ( '@tryghost/logging' ) ;
2023-03-07 17:34:43 +03:00
const { HtmlValidate } = require ( 'html-validate' ) ;
2023-07-11 11:00:19 +03:00
async function validateHtml ( html ) {
2023-03-07 17:34:43 +03:00
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
}
}
}
}
]
} ) ;
2023-07-11 11:00:19 +03:00
const report = await htmlvalidate . validateString ( html ) ;
2023-03-07 17:34:43 +03:00
// 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' ) ) ;
}
2022-11-29 13:27:17 +03:00
describe ( 'Email renderer' , function ( ) {
2023-01-11 14:13:13 +03:00
let logStub ;
beforeEach ( function ( ) {
logStub = sinon . stub ( logging , 'error' ) ;
} ) ;
afterEach ( function ( ) {
sinon . restore ( ) ;
} ) ;
2022-11-29 13:27:17 +03:00
describe ( 'buildReplacementDefinitions' , function ( ) {
2023-01-11 14:13:13 +03:00
let emailRenderer ;
let newsletter ;
let member ;
2023-10-24 13:35:47 +03:00
let labsEnabled = false ;
2023-01-11 14:13:13 +03:00
beforeEach ( function ( ) {
2023-10-24 13:35:47 +03:00
labsEnabled = false ;
2023-01-11 14:13:13 +03:00
emailRenderer = new EmailRenderer ( {
urlUtils : {
2023-03-22 13:52:41 +03:00
urlFor : ( ) => 'http://example.com/subdirectory/'
2023-03-14 19:11:24 +03:00
} ,
labs : {
2023-10-24 13:35:47 +03:00
isSet : ( ) => labsEnabled
2023-03-15 19:08:57 +03:00
} ,
settingsCache : {
get : ( key ) => {
if ( key === 'timezone' ) {
return 'UTC' ;
}
}
2023-01-11 14:13:13 +03:00
}
} ) ;
newsletter = createModel ( {
uuid : 'newsletteruuid'
} ) ;
member = {
id : '456' ,
uuid : 'myuuid' ,
name : 'Test User' ,
2023-03-15 19:08:57 +03:00
email : 'test@example.com' ,
2023-03-22 13:52:41 +03:00
createdAt : new Date ( 2023 , 2 , 13 , 12 , 0 ) ,
status : 'free'
2023-01-11 14:13:13 +03:00
} ;
2022-11-29 13:27:17 +03:00
} ) ;
2023-01-11 14:13:13 +03:00
it ( 'returns an empty list of replacements if nothing is used' , function ( ) {
2022-11-29 13:27:17 +03:00
const html = 'Hello world' ;
2023-03-07 17:34:43 +03:00
const replacements = emailRenderer . buildReplacementDefinitions ( { html , newsletterUuid : newsletter . get ( 'uuid' ) } ) ;
2022-11-29 13:27:17 +03:00
assert . equal ( replacements . length , 0 ) ;
} ) ;
it ( 'returns a replacement if it is used' , function ( ) {
const html = 'Hello world %%{uuid}%%' ;
2023-03-07 17:34:43 +03:00
const replacements = emailRenderer . buildReplacementDefinitions ( { html , newsletterUuid : newsletter . get ( 'uuid' ) } ) ;
2022-11-29 13:27:17 +03:00
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}%%' ;
2023-03-07 17:34:43 +03:00
const replacements = emailRenderer . buildReplacementDefinitions ( { html , newsletterUuid : newsletter . get ( 'uuid' ) } ) ;
2022-11-29 13:27:17 +03:00
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}%%,' ;
2023-03-07 17:34:43 +03:00
const replacements = emailRenderer . buildReplacementDefinitions ( { html , newsletterUuid : newsletter . get ( 'uuid' ) } ) ;
2022-11-29 13:27:17 +03:00
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' ) ;
} ) ;
2023-01-11 14:13:13 +03:00
it ( 'returns correct unsubscribe url' , function ( ) {
const html = 'Hello %%{unsubscribe_url}%%,' ;
2023-03-07 17:34:43 +03:00
const replacements = emailRenderer . buildReplacementDefinitions ( { html , newsletterUuid : newsletter . get ( 'uuid' ) } ) ;
2023-01-11 14:13:13 +03:00
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 ` ) ;
} ) ;
2023-10-24 13:35:47 +03:00
it ( 'returns correct list-unsubscribe value' , function ( ) {
labsEnabled = true ;
const html = 'Hello' ;
const replacements = emailRenderer . buildReplacementDefinitions ( { html , newsletterUuid : newsletter . get ( 'uuid' ) } ) ;
assert . equal ( replacements . length , 1 ) ;
assert . equal ( replacements [ 0 ] . token . toString ( ) , '/%%\\{list_unsubscribe\\}%%/g' ) ;
assert . equal ( replacements [ 0 ] . id , 'list_unsubscribe' ) ;
2023-10-25 17:16:31 +03:00
assert . equal ( replacements [ 0 ] . getValue ( member ) , ` http://example.com/subdirectory/unsubscribe/?uuid=myuuid&newsletter=newsletteruuid ` ) ;
2023-10-24 13:35:47 +03:00
} ) ;
2023-03-15 19:08:57 +03:00
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' ) ;
} ) ;
2023-03-22 17:15:34 +03:00
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 ) , '' ) ;
} ) ;
2023-03-15 19:08:57 +03:00
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' ) ;
} ) ;
2023-03-22 13:52:41 +03:00
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' ) ;
} ) ;
2023-03-24 10:51:20 +03:00
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' ) ;
} ) ;
2023-03-22 13:52:41 +03:00
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}%%,' ;
2023-03-24 10:51:20 +03:00
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
}
] ;
2023-03-22 13:52:41 +03:00
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' ) ;
2023-03-24 10:51:20 +03:00
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.' ) ;
2023-03-22 13:52:41 +03:00
} ) ;
2023-03-15 19:08:57 +03:00
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 ) , '' ) ;
} ) ;
2022-11-29 13:27:17 +03:00
it ( 'supports fallback values' , function ( ) {
const html = 'Hey %%{first_name, "there"}%%,' ;
2023-03-07 17:34:43 +03:00
const replacements = emailRenderer . buildReplacementDefinitions ( { html , newsletterUuid : newsletter . get ( 'uuid' ) } ) ;
2022-11-29 13:27:17 +03:00
assert . equal ( replacements . length , 1 ) ;
2023-03-07 17:34:43 +03:00
assert . equal ( replacements [ 0 ] . token . toString ( ) , '/%%\\{first_name, (?:"|")there(?:"|")\\}%%/g' ) ;
2022-12-02 12:49:01 +03:00
assert . equal ( replacements [ 0 ] . id , 'first_name_2' ) ;
2022-11-29 13:27:17 +03:00
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"}%%' ;
2023-03-07 17:34:43 +03:00
const replacements = emailRenderer . buildReplacementDefinitions ( { html , newsletterUuid : newsletter . get ( 'uuid' ) } ) ;
2022-11-29 13:27:17 +03:00
assert . equal ( replacements . length , 3 ) ;
2023-03-07 17:34:43 +03:00
assert . equal ( replacements [ 0 ] . token . toString ( ) , '/%%\\{first_name, (?:"|")there(?:"|")\\}%%/g' ) ;
2022-12-02 12:49:01 +03:00
assert . equal ( replacements [ 0 ] . id , 'first_name_2' ) ;
2022-11-29 13:27:17 +03:00
assert . equal ( replacements [ 0 ] . getValue ( member ) , 'Test' ) ;
// In case of empty name
assert . equal ( replacements [ 0 ] . getValue ( { name : '' } ) , 'there' ) ;
2023-03-07 17:34:43 +03:00
assert . equal ( replacements [ 1 ] . token . toString ( ) , '/%%\\{first_name, (?:"|")member(?:"|")\\}%%/g' ) ;
2022-12-02 12:49:01 +03:00
assert . equal ( replacements [ 1 ] . id , 'first_name_3' ) ;
2022-11-29 13:27:17 +03:00
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 : '' } ) , '' ) ;
} ) ;
} ) ;
2022-12-01 22:03:28 +03:00
2023-03-24 10:51:20 +03:00
describe ( 'isMemberTrialing' , function ( ) {
let emailRenderer ;
beforeEach ( function ( ) {
emailRenderer = new EmailRenderer ( {
urlUtils : {
urlFor : ( ) => 'http://example.com/subdirectory/'
} ,
labs : {
2023-10-24 13:35:47 +03:00
isSet : ( ) => false
2023-03-24 10:51:20 +03:00
} ,
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 ) ;
} ) ;
} ) ;
2023-03-22 13:52:41 +03:00
describe ( 'getMemberStatusText' , function ( ) {
let emailRenderer ;
beforeEach ( function ( ) {
emailRenderer = new EmailRenderer ( {
urlUtils : {
urlFor : ( ) => 'http://example.com/subdirectory/'
} ,
labs : {
2023-10-24 13:35:47 +03:00
isSet : ( ) => false
2023-03-22 13:52:41 +03:00
} ,
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 ) ;
2023-03-24 10:51:20 +03:00
assert . equal ( result , '' ) ;
2023-03-22 13:52:41 +03:00
} ) ;
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 , '' ) ;
} ) ;
} ) ;
2023-01-11 14:13:13 +03:00
describe ( 'getSubject' , function ( ) {
2022-12-01 22:03:28 +03:00
const emailRenderer = new EmailRenderer ( {
urlUtils : {
urlFor : ( ) => 'http://example.com'
2023-03-14 19:11:24 +03:00
} ,
labs : {
2023-10-24 13:35:47 +03:00
isSet : ( ) => false
2022-12-01 22:03:28 +03:00
}
} ) ;
it ( 'returns a post with correct subject from meta' , function ( ) {
2023-01-11 14:13:13 +03:00
const post = createModel ( {
posts _meta : createModel ( {
email _subject : 'Test Newsletter'
} ) ,
title : 'Sample Post' ,
loaded : [ 'posts_meta' ]
} ) ;
2022-12-01 22:03:28 +03:00
let response = emailRenderer . getSubject ( post ) ;
response . should . equal ( 'Test Newsletter' ) ;
} ) ;
it ( 'returns a post with correct subject from title' , function ( ) {
2023-01-11 14:13:13 +03:00
const post = createModel ( {
posts _meta : createModel ( {
email _subject : ''
} ) ,
title : 'Sample Post' ,
loaded : [ 'posts_meta' ]
} ) ;
2022-12-01 22:03:28 +03:00
let response = emailRenderer . getSubject ( post ) ;
2023-01-11 14:13:13 +03:00
response . should . equal ( 'Sample Post' ) ;
2022-12-01 22:03:28 +03:00
} ) ;
2024-01-03 19:08:56 +03:00
it ( 'adds [TEST] prefix for test emails' , function ( ) {
const post = createModel ( {
posts _meta : createModel ( {
email _subject : ''
} ) ,
title : 'Sample Post' ,
loaded : [ 'posts_meta' ]
} ) ;
let response = emailRenderer . getSubject ( post , true ) ;
response . should . equal ( '[TEST] Sample Post' ) ;
} ) ;
2022-12-01 22:03:28 +03:00
} ) ;
describe ( 'getFromAddress' , function ( ) {
2023-01-11 14:13:13 +03:00
let siteTitle = 'Test Blog' ;
2022-12-01 22:03:28 +03:00
let emailRenderer = new EmailRenderer ( {
settingsCache : {
get : ( key ) => {
if ( key === 'title' ) {
2023-01-11 14:13:13 +03:00
return siteTitle ;
2022-12-01 22:03:28 +03:00
}
}
} ,
settingsHelpers : {
getNoReplyAddress : ( ) => {
return 'reply@example.com' ;
}
2023-03-14 19:11:24 +03:00
} ,
labs : {
2023-10-24 13:35:47 +03:00
isSet : ( ) => false
2023-11-23 12:25:30 +03:00
} ,
emailAddressService : {
getAddress ( addresses ) {
return addresses ;
}
2022-12-01 22:03:28 +03:00
}
} ) ;
it ( 'returns correct from address for newsletter' , function ( ) {
2023-01-11 14:13:13 +03:00
const newsletter = createModel ( {
sender _email : 'ghost@example.com' ,
sender _name : 'Ghost'
} ) ;
const response = emailRenderer . getFromAddress ( { } , newsletter ) ;
2022-12-01 22:03:28 +03:00
response . should . equal ( '"Ghost" <ghost@example.com>' ) ;
2023-01-11 14:13:13 +03:00
} ) ;
2022-12-01 22:03:28 +03:00
2023-01-11 14:13:13 +03:00
it ( 'defaults to site title and domain' , function ( ) {
const newsletter = createModel ( {
sender _email : '' ,
sender _name : ''
} ) ;
const response = emailRenderer . getFromAddress ( { } , newsletter ) ;
2022-12-01 22:03:28 +03:00
response . should . equal ( '"Test Blog" <reply@example.com>' ) ;
} ) ;
2023-01-11 14:13:13 +03:00
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' ) ;
} ) ;
2022-12-01 22:03:28 +03:00
} ) ;
describe ( 'getReplyToAddress' , function ( ) {
2023-11-23 12:25:30 +03:00
let emailAddressService = {
getAddress ( addresses ) {
return addresses ;
2023-11-30 12:16:03 +03:00
} ,
managedEmailEnabled : true
2023-11-23 12:25:30 +03:00
} ;
2022-12-01 22:03:28 +03:00
let emailRenderer = new EmailRenderer ( {
settingsCache : {
get : ( key ) => {
if ( key === 'title' ) {
return 'Test Blog' ;
}
}
} ,
settingsHelpers : {
getMembersSupportAddress : ( ) => {
return 'support@example.com' ;
2023-01-11 14:13:13 +03:00
} ,
getNoReplyAddress : ( ) => {
return 'reply@example.com' ;
2022-12-01 22:03:28 +03:00
}
2023-03-14 19:11:24 +03:00
} ,
labs : {
2023-10-24 13:35:47 +03:00
isSet : ( ) => false
2023-11-23 12:25:30 +03:00
} ,
emailAddressService
2022-12-01 22:03:28 +03:00
} ) ;
2023-01-11 14:13:13 +03:00
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 ) ;
2022-12-01 22:03:28 +03:00
response . should . equal ( 'support@example.com' ) ;
} ) ;
2023-01-11 14:13:13 +03:00
2023-11-30 12:16:03 +03:00
it ( '[legacy] returns correct reply to address for newsletter' , function ( ) {
emailAddressService . managedEmailEnabled = false ;
const newsletter = createModel ( {
sender _email : 'ghost@example.com' ,
sender _name : 'Ghost' ,
sender _reply _to : 'newsletter'
} ) ;
const response = emailRenderer . getReplyToAddress ( { } , newsletter ) ;
assert . equal ( response , ` "Ghost" <ghost@example.com> ` ) ;
emailAddressService . managedEmailEnabled = true ;
} ) ;
it ( 'returns null when set to newsletter' , function ( ) {
emailAddressService . managedEmailEnabled = true ;
2023-01-11 14:13:13 +03:00
const newsletter = createModel ( {
sender _email : 'ghost@example.com' ,
sender _name : 'Ghost' ,
sender _reply _to : 'newsletter'
} ) ;
const response = emailRenderer . getReplyToAddress ( { } , newsletter ) ;
2023-11-30 12:16:03 +03:00
assert . equal ( response , null ) ;
2023-01-11 14:13:13 +03:00
} ) ;
2023-11-23 12:25:30 +03:00
it ( 'returns correct custom reply to address' , function ( ) {
const newsletter = createModel ( {
sender _email : 'ghost@example.com' ,
sender _name : 'Ghost' ,
sender _reply _to : 'anything@iwant.com'
} ) ;
const response = emailRenderer . getReplyToAddress ( { } , newsletter ) ;
assert . equal ( response , 'anything@iwant.com' ) ;
} ) ;
it ( 'handles removed replyto addresses' , function ( ) {
const newsletter = createModel ( {
sender _email : 'ghost@example.com' ,
sender _name : 'Ghost' ,
sender _reply _to : 'anything@iwant.com'
} ) ;
emailAddressService . getAddress = ( { from } ) => {
return {
from
} ;
} ;
const response = emailRenderer . getReplyToAddress ( { } , newsletter ) ;
assert . equal ( response , null ) ;
} ) ;
2022-12-01 22:03:28 +03:00
} ) ;
describe ( 'getSegments' , function ( ) {
let emailRenderer = new EmailRenderer ( {
renderers : {
lexical : {
render : ( ) => {
return '<p> Lexical Test</p>' ;
}
} ,
mobiledoc : {
render : ( ) => {
return '<p> Mobiledoc Test</p>' ;
}
}
2023-02-02 20:12:46 +03:00
} ,
getPostUrl : ( ) => {
return 'http://example.com/post-id' ;
2023-03-14 19:11:24 +03:00
} ,
labs : {
2023-10-24 13:35:47 +03:00
isSet : ( ) => false
2022-12-01 22:03:28 +03:00
}
} ) ;
2023-07-21 01:48:48 +03:00
it ( 'returns correct empty segment for post' , async function ( ) {
2022-12-01 22:03:28 +03:00
let post = {
get : ( key ) => {
if ( key === 'lexical' ) {
return '{}' ;
}
}
} ;
2023-07-21 01:48:48 +03:00
let response = await emailRenderer . getSegments ( post ) ;
2022-12-01 22:03:28 +03:00
response . should . eql ( [ null ] ) ;
post = {
get : ( key ) => {
if ( key === 'mobiledoc' ) {
return '{}' ;
}
}
} ;
2023-07-21 01:48:48 +03:00
response = await emailRenderer . getSegments ( post ) ;
2022-12-01 22:03:28 +03:00
response . should . eql ( [ null ] ) ;
} ) ;
2023-07-21 01:48:48 +03:00
it ( 'returns correct segments for post with members only card' , async function ( ) {
2022-12-01 22:03:28 +03:00
emailRenderer = new EmailRenderer ( {
renderers : {
lexical : {
render : ( ) => {
return '<p> Lexical Test <!--members-only--> members only section</p>' ;
}
}
2023-02-02 20:12:46 +03:00
} ,
getPostUrl : ( ) => {
return 'http://example.com/post-id' ;
2023-03-14 19:11:24 +03:00
} ,
labs : {
2023-10-24 13:35:47 +03:00
isSet : ( ) => false
2022-12-01 22:03:28 +03:00
}
} ) ;
let post = {
get : ( key ) => {
if ( key === 'lexical' ) {
return '{}' ;
}
}
} ;
2023-07-21 01:48:48 +03:00
let response = await emailRenderer . getSegments ( post ) ;
2022-12-01 22:03:28 +03:00
response . should . eql ( [ 'status:free' , 'status:-free' ] ) ;
} ) ;
2023-07-21 01:48:48 +03:00
it ( 'returns correct segments for post with email card' , async function ( ) {
2022-12-01 22:03:28 +03:00
emailRenderer = new EmailRenderer ( {
renderers : {
lexical : {
render : ( ) => {
return '<html> <div> Lexical Test </div> <div data-gh-segment="status:-free"> members only section</div> </html>' ;
}
}
2023-02-02 20:12:46 +03:00
} ,
getPostUrl : ( ) => {
return 'http://example.com/post-id' ;
2023-03-14 19:11:24 +03:00
} ,
labs : {
2023-10-24 13:35:47 +03:00
isSet : ( ) => false
2022-12-01 22:03:28 +03:00
}
} ) ;
let post = {
get : ( key ) => {
if ( key === 'lexical' ) {
return '{}' ;
}
}
} ;
2023-07-21 01:48:48 +03:00
let response = await emailRenderer . getSegments ( post ) ;
2023-01-25 16:56:37 +03:00
response . should . eql ( [ 'status:free' , 'status:-free' ] ) ;
2022-12-01 22:03:28 +03:00
} ) ;
} ) ;
describe ( 'renderBody' , function ( ) {
2023-06-29 11:40:04 +03:00
let renderedPost ;
2023-03-07 17:34:43 +03:00
let postUrl = 'http://example.com' ;
let customSettings = { } ;
2023-03-24 16:14:55 +03:00
let emailRenderer ;
2023-01-11 14:13:13 +03:00
let basePost ;
2023-03-24 16:14:55 +03:00
let addTrackingToUrlStub ;
2023-03-29 07:03:49 +03:00
let labsEnabled ;
2022-12-01 22:03:28 +03:00
2023-01-11 14:13:13 +03:00
beforeEach ( function ( ) {
2023-06-29 11:40:04 +03:00
renderedPost = '<p>Lexical Test</p><img class="is-light-background" src="test-dark" /><img class="is-dark-background" src="test-light" />' ;
2023-03-29 07:03:49 +03:00
labsEnabled = true ;
2023-01-11 14:13:13 +03:00
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' ]
2022-12-01 22:03:28 +03:00
} ;
2023-03-07 17:34:43 +03:00
postUrl = 'http://example.com' ;
customSettings = { } ;
2023-03-24 16:14:55 +03:00
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 : {
2023-03-29 07:03:49 +03:00
isSet : ( ) => labsEnabled
2023-03-24 16:14:55 +03:00
}
} ) ;
2023-01-11 14:13:13 +03:00
} ) ;
2022-12-01 22:03:28 +03:00
2023-03-29 07:03:49 +03:00
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
) ;
} ) ;
2023-01-11 14:13:13 +03:00
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 ,
2023-03-14 13:29:43 +03:00
feedback _enabled : true ,
show _post _title _section : true
2023-01-11 14:13:13 +03:00
} ) ;
const segment = null ;
const options = { } ;
2022-12-01 22:03:28 +03:00
let response = await emailRenderer . renderBody (
post ,
newsletter ,
segment ,
options
) ;
2022-12-14 13:24:26 +03:00
const $ = cheerio . load ( response . html ) ;
2022-12-01 22:03:28 +03:00
response . plaintext . should . containEql ( 'Test Post' ) ;
2023-01-11 14:13:13 +03:00
// Unsubscribe button included
2022-12-01 22:03:28 +03:00
response . plaintext . should . containEql ( 'Unsubscribe [%%{unsubscribe_url}%%]' ) ;
response . html . should . containEql ( 'Unsubscribe' ) ;
2023-10-24 13:35:47 +03:00
response . replacements . length . should . eql ( 3 ) ;
2022-12-01 22:03:28 +03:00
response . replacements . should . match ( [
2023-01-11 14:13:13 +03:00
{
id : 'uuid'
} ,
2022-12-01 22:03:28 +03:00
{
id : 'unsubscribe_url' ,
token : /%%\{unsubscribe_url\}%%/g
2023-10-24 13:35:47 +03:00
} ,
{
id : 'list_unsubscribe'
2022-12-01 22:03:28 +03:00
}
] ) ;
2023-01-11 14:13:13 +03:00
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 ,
2023-03-14 13:29:43 +03:00
feedback _enabled : true ,
show _post _title _section : true
2023-01-11 14:13:13 +03:00
} ) ;
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' ) ;
} ) ;
2023-06-29 11:40:04 +03:00
it ( 'does not include members-only content in preheader for non-members' , async function ( ) {
renderedPost = '<div> Lexical Test </div> some text for both <!--members-only--> finishing part only for members' ;
let post = {
related : sinon . stub ( ) ,
get : ( key ) => {
if ( key === 'lexical' ) {
return '{}' ;
}
if ( key === 'visibility' ) {
return 'paid' ;
}
if ( key === 'plaintext' ) {
return 'foobarbaz' ;
}
} ,
getLazyRelation : sinon . stub ( )
} ;
let newsletter = {
get : sinon . stub ( )
} ;
let response = await emailRenderer . renderBody (
post ,
newsletter ,
'status:free' ,
{ }
) ;
const $ = cheerio . load ( response . html ) ;
should ( $ ( '.preheader' ) . text ( ) ) . eql ( 'Lexical Test some text for both' ) ;
} ) ;
it ( 'does not include paid segmented content in preheader for non-paying members' , async function ( ) {
renderedPost = '<div> Lexical Test </div> <div data-gh-segment="status:-free"> members only section</div> some text for both' ;
let post = {
related : sinon . stub ( ) ,
get : ( key ) => {
if ( key === 'lexical' ) {
return '{}' ;
}
if ( key === 'visibility' ) {
return 'public' ;
}
if ( key === 'plaintext' ) {
return 'foobarbaz' ;
}
} ,
getLazyRelation : sinon . stub ( )
} ;
let newsletter = {
get : sinon . stub ( )
} ;
let response = await emailRenderer . renderBody (
post ,
newsletter ,
'status:free' ,
{ }
) ;
const $ = cheerio . load ( response . html ) ;
should ( $ ( '.preheader' ) . text ( ) ) . eql ( 'Lexical Test some text for both' ) ;
} ) ;
2023-01-11 14:13:13 +03:00
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 ,
2023-03-14 13:29:43 +03:00
feedback _enabled : true ,
show _post _title _section : true
2023-01-11 14:13:13 +03:00
} ) ;
const segment = null ;
const options = { } ;
let response = await emailRenderer . renderBody (
post ,
newsletter ,
segment ,
options
) ;
assert . match ( response . html , /By A & 2 others/ ) ;
assert . match ( response . plaintext , /By A & 2 others/ ) ;
} ) ;
it ( 'includes header icon, title, name' , async function ( ) {
const post = createModel ( basePost ) ;
const newsletter = createModel ( {
header _image : null ,
name : 'Test Newsletter' ,
show _badge : false ,
feedback _enabled : true ,
show _header _icon : true ,
show _header _title : true ,
2023-03-14 13:29:43 +03:00
show _header _name : true ,
show _post _title _section : true
2023-01-11 14:13:13 +03:00
} ) ;
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 ,
2023-03-14 13:29:43 +03:00
show _header _name : true ,
show _post _title _section : true
2023-01-11 14:13:13 +03:00
} ) ;
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' ) ;
} ) ;
2023-04-05 13:51:18 +03:00
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/ ) ;
} ) ;
2024-03-26 19:51:23 +03:00
it ( 'replaces all links except the unsubscribe, feedback and powered by Ghost links' , async function ( ) {
2023-01-11 14:13:13 +03:00
const post = createModel ( basePost ) ;
const newsletter = createModel ( {
header _image : null ,
name : 'Test Newsletter' ,
show _badge : true ,
2023-03-14 13:29:43 +03:00
feedback _enabled : true ,
show _post _title _section : true
2023-01-11 14:13:13 +03:00
} ) ;
const segment = null ;
const options = {
clickTrackingEnabled : true
} ;
2023-08-08 14:22:56 +03:00
renderedPost = '<p>Lexical Test</p><p><a href="https://external-domain.com/?ref=123">Hello</a><a href="https://encoded-link.com?code=test">Hello</a><a href="https://example.com/?ref=123"><img src="example" /></a><a href="#">Ignore me</a></p>' ;
2023-03-24 16:14:55 +03:00
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 ) ;
2023-08-08 14:22:56 +03:00
if ( href === '#' ) {
continue ;
}
2023-03-24 16:14:55 +03:00
if ( href . includes ( 'unsubscribe_url' ) ) {
href . should . eql ( '%%{unsubscribe_url}%%' ) ;
} else if ( href . includes ( 'feedback-link.com' ) ) {
href . should . containEql ( '%%{uuid}%%' ) ;
2024-03-26 19:51:23 +03:00
} else if ( href . includes ( 'https://ghost.org/?via=pbg-newsletter' ) ) {
href . should . not . containEql ( 'tracked-link.com' ) ;
2023-03-24 16:14:55 +03:00
} 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 , [
2023-03-24 16:54:16 +03:00
` http://tracked-link.com/?m=%%{uuid}%%&url=http%3A%2F%2Fexample.com%2F%3Fsource_tracking%3DTest%2BNewsletter%26post_tracking%3Dadded ` ,
2023-03-24 16:14:55 +03:00
` 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 ` ,
2023-08-08 14:22:56 +03:00
'#' ,
2023-03-24 16:14:55 +03:00
` 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}%% ` ,
2024-03-26 19:51:23 +03:00
` https://ghost.org/?via=pbg-newsletter&source_tracking=site `
2023-03-24 16:14:55 +03:00
] ) ;
// Check uuid in replacements
2023-10-24 13:35:47 +03:00
response . replacements . length . should . eql ( 3 ) ;
2023-03-24 16:14:55 +03:00
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 ) ;
2023-10-24 13:35:47 +03:00
response . replacements [ 2 ] . id . should . eql ( 'list_unsubscribe' ) ;
2023-03-24 16:14:55 +03:00
} ) ;
2023-08-08 14:22:56 +03:00
it ( 'replaces all relative links if click tracking is disabled' , 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 : false
} ;
renderedPost = '<p>Lexical Test</p><p><a href="#relative-test">Hello</a><a href="#">Ignore me</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 ) ;
}
// 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://example.com/' ,
'http://example.com/' ,
'http://example.com/' ,
'http://example.com/#relative-test' ,
'#' ,
'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}%%' ,
2024-03-26 19:51:23 +03:00
'https://ghost.org/?via=pbg-newsletter'
2023-08-08 14:22:56 +03:00
] ) ;
} ) ;
2023-03-24 16:14:55 +03:00
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
} ;
2023-01-11 14:13:13 +03:00
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}%%' ) ;
2024-03-26 19:51:23 +03:00
} else if ( href . includes ( 'https://ghost.org/?via=pbg-newsletter' ) ) {
href . should . not . containEql ( 'tracked-link.com' ) ;
2023-01-11 14:13:13 +03:00
} 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 , [
2023-03-24 16:54:16 +03:00
` http://tracked-link.com/?m=%%{uuid}%%&url=http%3A%2F%2Fexample.com%2F%3Fsource_tracking%3DTest%2BNewsletter%26post_tracking%3Dadded ` ,
2023-01-11 14:13:13 +03:00
` 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}%% ` ,
2023-03-17 18:09:50 +03:00
` http://feedback-link.com/?score=0&uuid=%%{uuid}%% ` ,
` http://feedback-link.com/?score=1&uuid=%%{uuid}%% ` ,
2023-01-11 14:13:13 +03:00
` http://feedback-link.com/?score=0&uuid=%%{uuid}%% ` ,
` %%{unsubscribe_url}%% ` ,
2024-03-26 19:51:23 +03:00
` https://ghost.org/?via=pbg-newsletter&source_tracking=site `
2023-01-11 14:13:13 +03:00
] ) ;
// Check uuid in replacements
2023-10-24 13:35:47 +03:00
response . replacements . length . should . eql ( 3 ) ;
2023-01-11 14:13:13 +03:00
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 ) ;
2023-10-24 13:35:47 +03:00
response . replacements [ 2 ] . id . should . eql ( 'list_unsubscribe' ) ;
2022-12-01 22:03:28 +03:00
} ) ;
2022-12-02 12:48:29 +03:00
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 ;
}
2023-06-29 13:47:17 +03:00
if ( key === 'show_post_title_section' ) {
return true ;
}
2022-12-02 12:48:29 +03:00
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' ) ;
2023-06-29 13:47:17 +03:00
// Check contains the post name twice
assert . equal ( response . html . match ( /Test Post/g ) . length , 3 , 'Should contain the post name 3 times: in the title element, the preheader and in the post title section' ) ;
2022-12-02 12:48:29 +03:00
response . html . should . containEql ( 'Unsubscribe' ) ;
response . html . should . containEql ( 'http://example.com' ) ;
2023-10-24 13:35:47 +03:00
response . replacements . length . should . eql ( 3 ) ;
2022-12-02 12:48:29 +03:00
response . replacements . should . match ( [
2023-01-11 14:13:13 +03:00
{
id : 'uuid'
} ,
2022-12-02 12:48:29 +03:00
{
id : 'unsubscribe_url' ,
token : /%%\{unsubscribe_url\}%%/g
2023-10-24 13:35:47 +03:00
} ,
{
id : 'list_unsubscribe'
2022-12-02 12:48:29 +03:00
}
] ) ;
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' ) ;
} ) ;
2023-03-07 17:34:43 +03:00
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
) ;
2023-07-11 11:00:19 +03:00
await validateHtml ( response . html ) ;
2023-03-07 17:34:43 +03:00
// 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' ) ;
} ) ;
2023-07-25 04:33:56 +03:00
it ( 'does not replace img height and width with auto from css' , async function ( ) {
const post = createModel ( basePost ) ;
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 = { } ;
renderedPost = '<p>This is the post.</p><figure class="kg-card kg-image-card"><img src="__GHOST_URL__/content/images/2023/07/audio-sample_thumb.png" class="kg-image" alt loading="lazy" width="248" height="248"></figure><p>Theres an image!</p>' ;
const response = await emailRenderer . renderBody (
post ,
newsletter ,
segment ,
options
) ;
// console.log(response.html);
assert . equal ( response . html . includes ( 'width="248" height="248"' ) , true , 'Should not replace img height and width with auto from css' ) ;
assert . equal ( response . html . includes ( 'width="auto" height="auto"' ) , false , 'Should not replace img height and width with auto from css' ) ;
} ) ;
2022-12-01 22:03:28 +03:00
} ) ;
2022-12-09 13:17:22 +03:00
2023-01-11 14:13:13 +03:00
describe ( 'getTemplateData' , function ( ) {
let settings = { } ;
2023-03-14 19:11:24 +03:00
let labsEnabled = true ;
2023-03-20 16:30:42 +03:00
let emailRenderer ;
2023-01-11 14:13:13 +03:00
beforeEach ( function ( ) {
2023-07-20 10:46:27 +03:00
settings = {
timezone : 'Etc/UTC'
} ;
2023-03-14 19:11:24 +03:00
labsEnabled = true ;
2023-03-20 16:30:42 +03:00
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' ) ,
2023-03-22 18:09:59 +03:00
custom _excerpt : 'Super long custom excerpt. Super long custom excerpt. Super long custom excerpt. Super long custom excerpt. Super long custom excerpt.' ,
2023-03-20 16:30:42 +03:00
feature _image : 'http://example.com/image.jpg'
} ,
{
title : 'Test Post 2' ,
published _at : new Date ( '2018-01-01T00:00:00.000Z' ) ,
2023-03-22 18:09:59 +03:00
feature _image : null ,
plaintext : ''
2023-03-20 16:30:42 +03:00
} ,
{
title : 'Test Post 3' ,
published _at : null , // required for full test coverage
2023-03-22 18:09:59 +03:00
feature _image : null ,
plaintext : 'Nothing special.'
2023-03-20 16:30:42 +03:00
}
]
} )
}
} ) ;
2023-01-11 14:13:13 +03:00
} ) ;
2023-03-29 07:03:49 +03:00
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' } ,
2023-04-03 11:46:47 +03:00
{ input : 'auto' , expected : '#FFFFFF' , background _color : '#15212A' } ,
{ input : 'auto' , expected : '#000000' , background _color : '#ffffff' } ,
2023-03-29 07:03:49 +03:00
{ input : 'light' , expected : null } ,
{ input : 'accent' , expected : settings . accent _color } ,
{ input : 'transparent' , expected : null }
] ;
for ( const test of tests ) {
const data = await templateDataWithSettings ( {
2023-04-03 11:46:47 +03:00
border _color : test . input ,
background _color : test . background _color
2023-03-29 07:03:49 +03:00
} ) ;
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 ) ;
}
} ) ;
2023-01-11 14:13:13 +03:00
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'
} ) ;
} ) ;
2023-03-14 19:11:24 +03:00
2023-03-17 11:01:35 +03:00
it ( 'show comment CTA is enabled if labs disabled' , async function ( ) {
2023-03-14 19:11:24 +03:00
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 } ) ;
2023-03-17 11:01:35 +03:00
assert . equal ( data . newsletter . showCommentCta , true ) ;
2023-03-14 19:11:24 +03:00
} ) ;
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 ) ;
} ) ;
2023-03-15 19:08:57 +03:00
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 ) ;
} ) ;
2023-03-20 16:30:42 +03:00
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 ,
[
{
2023-03-24 14:14:00 +03:00
excerpt : 'Super long custom excerpt. Super long custom excerpt. Super<span class="mobile-only"> long custom excerpt. Super long custom excer</span>…' ,
2023-03-20 16:30:42 +03:00
title : 'Test Post 1' ,
2023-03-22 17:52:21 +03:00
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
}
2023-03-20 16:30:42 +03:00
} ,
{
featureImage : null ,
2023-03-22 17:52:21 +03:00
featureImageMobile : null ,
2023-03-22 18:09:59 +03:00
excerpt : '' ,
2023-03-20 16:30:42 +03:00
title : 'Test Post 2' ,
url : 'http://example.com'
} ,
{
featureImage : null ,
2023-03-22 17:52:21 +03:00
featureImageMobile : null ,
2023-03-22 18:09:59 +03:00
excerpt : 'Nothing special.' ,
2023-03-20 16:30:42 +03:00
title : 'Test Post 3' ,
url : 'http://example.com'
}
] ) ;
} ) ;
2023-01-11 14:13:13 +03:00
} ) ;
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 ` ) ;
} ) ;
} ) ;
2023-03-22 18:09:59 +03:00
describe ( 'truncateText' , function ( ) {
it ( 'works for null' , async function ( ) {
const emailRenderer = new EmailRenderer ( { } ) ;
assert . equal ( emailRenderer . truncateText ( null , 100 ) , '' ) ;
} ) ;
} ) ;
2023-03-24 14:14:00 +03:00
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 ) , '' ) ;
} ) ;
} ) ;
2022-12-09 13:17:22 +03:00
describe ( 'limitImageWidth' , function ( ) {
it ( 'Limits width of local images' , async function ( ) {
const emailRenderer = new EmailRenderer ( {
imageSize : {
getImageSizeFromUrl ( ) {
return {
2023-03-22 16:17:01 +03:00
width : 2000 ,
height : 1000
2022-12-09 13:17:22 +03:00
} ;
}
} ,
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 ) ;
2023-03-22 16:17:01 +03:00
assert . equal ( response . height , 300 ) ;
2022-12-09 13:17:22 +03:00
assert . equal ( response . href , 'http://your-blog.com/content/images/size/w1200/2017/01/02/example.png' ) ;
} ) ;
2023-03-22 16:17:01 +03:00
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' ) ;
} ) ;
2023-01-11 14:13:13 +03:00
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 ) ;
} ) ;
2022-12-09 13:17:22 +03:00
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 ) ;
2023-03-22 16:17:01 +03:00
assert . equal ( response . height , null ) ;
2022-12-09 13:17:22 +03:00
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' ) ;
} ) ;
2023-03-22 16:17:01 +03:00
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' ) ;
} ) ;
2022-12-09 13:17:22 +03:00
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' ) ;
} ) ;
} ) ;
2022-11-29 13:27:17 +03:00
} ) ;