[web] update email base templates (#33791)

* update email template design
* gate email template on split test

GitOrigin-RevId: 2e0a1d9abf11a0c3f16e103511191d43d542b8a4
This commit is contained in:
Kristina
2026-05-28 08:05:59 +00:00
committed by Copybot
parent fa26367aa9
commit 78dd0a8681
11 changed files with 457 additions and 72 deletions
@@ -0,0 +1,28 @@
import _ from 'lodash'
import { fontFamily, colors } from '../emailStyles.mjs'
export default _.template(`\
<% if (title) { %>
<h2 style="margin: 0 0 16px 0; font-family: ${fontFamily}; font-size: 30px; font-weight: 600; color: ${colors.textDark}; line-height: 40px;">
<%= title %>
</h2>
<% } %>
<% if (greeting) { %>
<p style="margin: 0 0 16px 0;">
<%= greeting %>
</p>
<% } %>
<% (message).forEach(function(paragraph) { %>
<p class="force-overleaf-style" style="margin: 0 0 16px 0;">
<%= paragraph %>
</p>
<% }) %>
<% if (highlightedText) { %>
<div style="text-align: center; color: ${colors.textDark}; font-size: 20px; margin: 16px 0; padding: 16px 8px; border-radius: 8px; background-color: ${colors.background};">
<b><%= highlightedText %></b>
</div>
<% } %>\
`)
@@ -0,0 +1,65 @@
import _ from 'lodash'
import { fontFamily, colors } from '../emailStyles.mjs'
export default _.template(`\
<% if (title) { %>
<h2 style="margin: 0 0 16px 0; font-family: ${fontFamily}; font-size: 30px; font-weight: 600; color: ${colors.textDark}; line-height: 40px;">
<%= title %>
</h2>
<% } %>
<% if (greeting) { %>
<p style="margin: 0 0 16px 0;">
<%= greeting %>
</p>
<% } %>
<% (message).forEach(function(paragraph) { %>
<p class="force-overleaf-style" style="margin: 0 0 16px 0;">
<%= paragraph %>
</p>
<% }) %>
<table role="presentation" border="0" cellpadding="0" cellspacing="0" style="margin: 32px 0;">
<tr>
<td bgcolor="${colors.ctaGreen}" style="background-color: ${colors.ctaGreen}; border-radius: 9999px; padding: 6px 16px; text-align: center;">
<a href="<%= ctaURL %>" style="color: ${colors.ctaText}; display: block; font-family: ${fontFamily}; font-size: 16px; font-weight: 600; line-height: 24px; text-align: center; text-decoration: none; -webkit-hyphens: none; -ms-hyphens: none; hyphens: none;">
<%= ctaText %>
</a>
</td>
</tr>
</table>
<% if (secondaryMessage && secondaryMessage.length > 0) { %>
<% (secondaryMessage).forEach(function(paragraph) { %>
<p class="force-overleaf-style" style="margin: 0 0 16px 0;">
<%= paragraph %>
</p>
<% }) %>
<% } %>
<p style="margin: 0 0 4px 0; font-size: 14px; line-height: 20px; color: ${colors.textMuted};">
If the button above does not appear, please copy and paste this link into your browser's address bar:
</p>
<p style="margin: 0; font-size: 14px; line-height: 20px; word-break: break-all;">
<a href="<%= ctaURL %>" style="color: ${colors.linkGreen}; text-decoration: none;"><%= ctaURL %></a>
</p>
<% if (gmailGoToAction) { %>
<script type="application/ld+json">
<%=
StringHelper.stringifyJsonForScript({
"@context": "http://schema.org",
"@type": "EmailMessage",
"potentialAction": {
"@type": "ViewAction",
"target": gmailGoToAction.target,
"url": gmailGoToAction.target,
"name": gmailGoToAction.name
},
"description": gmailGoToAction.description
})
%>
</script>
<% } %>\
`)
@@ -4,9 +4,12 @@ import moment from 'moment'
import EmailMessageHelper from './EmailMessageHelper.mjs'
import StringHelper from '../Helpers/StringHelper.mjs'
import BaseEmailLayout from './Layouts/BaseEmailLayout.mjs'
import UpdatedBaseEmailLayout from './Layouts/UpdatedBaseEmailLayout.mjs'
import SpamSafe from './SpamSafe.mjs'
import ctaEmailBody from './Bodies/cta-email.mjs'
import updatedCtaEmailBody from './Bodies/updated-cta-email.mjs'
import NoCTAEmailBody from './Bodies/NoCTAEmailBody.mjs'
import UpdatedNoCTAEmailBody from './Bodies/UpdatedNoCTAEmailBody.mjs'
function _emailBodyPlainText(content, opts, ctaEmail) {
let emailBody = `${content.greeting(opts, true)}`
@@ -75,7 +78,12 @@ function ctaTemplate(content) {
subject(opts) {
return content.subject(opts)
},
layout: BaseEmailLayout,
layout(opts) {
const layoutFn = opts.useNewEmailDesign
? UpdatedBaseEmailLayout
: BaseEmailLayout
return layoutFn(opts)
},
footerMessage(opts) {
return content.footerMessage(opts)
},
@@ -83,7 +91,8 @@ function ctaTemplate(content) {
return _emailBodyPlainText(content, opts, true)
},
compiledTemplate(opts) {
return ctaEmailBody({
const bodyFn = opts.useNewEmailDesign ? updatedCtaEmailBody : ctaEmailBody
return bodyFn({
title: content.title(opts),
greeting: content.greeting(opts),
message: content.message(opts),
@@ -108,7 +117,12 @@ function NoCTAEmailTemplate(content) {
subject(opts) {
return content.subject(opts)
},
layout: BaseEmailLayout,
layout(opts) {
const layoutFn = opts.useNewEmailDesign
? UpdatedBaseEmailLayout
: BaseEmailLayout
return layoutFn(opts)
},
plainTextTemplate(opts) {
return `\
${content.greeting(opts)}
@@ -120,7 +134,10 @@ The ${settings.appName} Team - ${settings.siteUrl}\
`
},
compiledTemplate(opts) {
return NoCTAEmailBody({
const bodyFn = opts.useNewEmailDesign
? UpdatedNoCTAEmailBody
: NoCTAEmailBody
return bodyFn({
title:
typeof content.title === 'function' ? content.title(opts) : undefined,
greeting: content.greeting(opts),
@@ -137,7 +154,6 @@ The ${settings.appName} Team - ${settings.siteUrl}\
function buildEmail(templateName, opts) {
const template = templates[templateName]
opts.siteUrl = settings.siteUrl
opts.body = template.compiledTemplate(opts)
opts.footerMessage = template.footerMessage
? template.footerMessage(opts)
@@ -189,7 +205,7 @@ templates.canceledSubscription = ctaTemplate({
return ['Thank you in advance!']
},
ctaText() {
return 'Leave Feedback'
return 'Leave feedback'
},
ctaURL(opts) {
return 'https://docs.google.com/forms/d/e/1FAIpQLSfa7z_s-cucRRXm70N4jEcSbFsZeb0yuKThHGQL8ySEaQzF0Q/viewform?usp=sf_link'
@@ -394,7 +410,7 @@ templates.reconfirmEmail = ctaTemplate({
]
},
ctaText() {
return 'Reconfirm Email'
return 'Reconfirm email'
},
ctaURL(opts) {
return opts.confirmEmailUrl
@@ -894,7 +910,7 @@ templates.SAMLDataCleared = ctaTemplate({
]
},
ctaText(opts) {
return 'Update my Emails and affiliations'
return 'Update my emails and affiliations'
},
ctaURL(opts) {
return `${settings.siteUrl}/user/settings`
@@ -4,14 +4,37 @@ import logger from '@overleaf/logger'
import EmailBuilder from './EmailBuilder.mjs'
import EmailSender from './EmailSender.mjs'
import Queues from '../../infrastructure/Queues.mjs'
import SplitTestHandler from '../SplitTests/SplitTestHandler.mjs'
import UserGetter from '../User/UserGetter.mjs'
const EMAIL_SETTINGS = Settings.email || {}
/**
* @param {string} emailType
* @param {opts} any
* @param {object} opts
*/
async function sendEmail(emailType, opts) {
if (opts.to) {
try {
const user = await UserGetter.promises.getUserByAnyEmail(opts.to, {
_id: 1,
})
if (user) {
const { variant } =
await SplitTestHandler.promises.getAssignmentForUser(
user._id.toString(),
'new-email-design'
)
opts.useNewEmailDesign = variant === 'enabled'
}
} catch (err) {
logger.warn(
{ err },
'failed to get new-email-design split test assignment'
)
}
}
const email = EmailBuilder.buildEmail(emailType, opts)
if (email.type === 'lifecycle' && !EMAIL_SETTINGS.lifecycle) {
return
@@ -0,0 +1,169 @@
import _ from 'lodash'
import settings from '@overleaf/settings'
import { fontFamily, colors } from '../emailStyles.mjs'
const _isSaas = settings.env === 'saas'
const _imageBase = settings.cdn?.web?.host || settings.siteUrl || null
let _logoHtml = `<span style="font-family: ${fontFamily}; font-size: 30px; font-weight: bold; color: ${colors.logoGreen};">${settings.appName}</span>`
let _taglineHtml = ''
if (_isSaas && _imageBase) {
_logoHtml = `
<a href="${settings.siteUrl}" style="text-decoration: none; display: inline-block; border: 0;">
<img
src="${_imageBase}/img/ol-brand/email-logo@2x.png"
alt="${settings.appName}"
width="139"
height="40"
style="display: block; border: 0; width: 139px; height: 40px;"
>
</a>
`
_taglineHtml = `
<!--[if mso]>
<img
src="${_imageBase}/img/ol-brand/email-footer-tagline@2x.png"
alt="The home of scientific and technical writing"
width="452"
height="25"
style="display: block; border: 0;"
/>
<![endif]-->
<!--[if !mso]><!-->
<img
src="${_imageBase}/img/ol-brand/email-footer-tagline@2x.png"
alt="The home of scientific and technical writing"
style="display: block; border: 0; width: 100%; max-width: 452px; height: auto;"
>
<!--<![endif]-->
`
}
const _customFooter = settings.email?.template?.customFooter || ''
export default _.template(`\
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" lang="en" xml:lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="x-apple-disable-message-reformatting">
<!--[if mso]>
<style type="text/css">
body, table, td, p, a, li { font-family: Arial, Helvetica, sans-serif !important; }
</style>
<![endif]-->
<style>
body { margin: 0; padding: 0; background-color: ${colors.background}; }
img { border: 0; display: block; outline: none; text-decoration: none; }
.email-layout-table { border-collapse: collapse !important; }
a[x-apple-data-detectors] { color: inherit !important; text-decoration: none !important; }
.force-overleaf-style a,
.force-overleaf-style a[href] {
color: ${colors.linkGreen} !important;
text-decoration: underline !important;
-moz-hyphens: none;
-ms-hyphens: none;
-webkit-hyphens: none;
hyphens: none;
}
.force-overleaf-style a:visited,
.force-overleaf-style a[href]:visited { color: ${colors.linkGreen}; }
.force-overleaf-style a:hover,
.force-overleaf-style a[href]:hover { color: ${colors.linkHover}; }
@media only screen and (min-width: 621px) {
.email-card-inner { padding: 56px !important; }
}
@media only screen and (max-width: 620px) {
.email-container { width: 100% !important; max-width: 100% !important; }
.email-outer-cell { padding: 0 10px !important; }
.email-header-cell { padding: 16px 0 !important; }
}
</style>
</head>
<body bgcolor="${colors.background}" style="margin: 0; padding: 0; background-color: ${colors.background}; font-family: ${fontFamily};">
<!-- Prevent Gmail iOS font size manipulation -->
<div style="display: none; white-space: nowrap; font: 15px courier; line-height: 0;">
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
</div>
<table class="email-layout-table" role="presentation" border="0" cellpadding="0" cellspacing="0" width="100%" bgcolor="${colors.background}" style="background-color: ${colors.background}; min-width: 100%;">
<tr>
<td class="email-outer-cell" align="center" valign="top" style="padding: 40px 10px;">
<!--[if mso]>
<table role="presentation" border="0" cellpadding="0" cellspacing="0" width="600" align="center"><tr><td>
<![endif]-->
<table class="email-container email-layout-table" role="presentation" border="0" cellpadding="0" cellspacing="0" width="100%" style="max-width: 600px;">
<!-- Logo header -->
<tr>
<td class="email-header-cell" style="padding: 0 0 24px 0;">
${_logoHtml}
</td>
</tr>
<!-- White content card -->
<tr>
<td bgcolor="#ffffff" style="background-color: #ffffff; border-radius: 8px; overflow: hidden;">
<table class="email-layout-table" role="presentation" border="0" cellpadding="0" cellspacing="0" width="100%">
<tr>
<td
class="email-card-inner"
style="padding: 32px 24px; font-family: ${fontFamily}; font-size: 16px; line-height: 1.5; color: ${colors.textDark};"
>
<%= body %>
</td>
</tr>
</table>
</td>
</tr>
<!-- Footer spacer -->
<tr>
<td style="height: 24px; font-size: 24px; line-height: 24px; mso-line-height-rule: exactly;">&nbsp;</td>
</tr>
<!-- Footer tagline -->
<tr>
<td align="center" style="padding: 0 0 48px 0;">
${_taglineHtml}
</td>
</tr>
<!-- Footer site info -->
${
_customFooter
? `<tr>
<td align="center" style="padding: 0 0 32px 0; font-family: ${fontFamily}; font-size: 13px; color: ${colors.textMuted}; line-height: 1.5;">
${_customFooter}
</td>
</tr>`
: ''
}
<% if (footerMessage) { %>
<tr>
<td align="center" class="force-overleaf-style" style="padding: 0 0 32px 0; font-family: ${fontFamily}; font-size: 14px; color: ${colors.textMuted}; line-height: 20px;">
<%= footerMessage %>
</td>
</tr>
<% } %>
</table>
<!--[if mso]>
</td></tr></table>
<![endif]-->
</td>
</tr>
</table>
</body>
</html>\
`)
@@ -0,0 +1,12 @@
export const fontFamily = 'Arial, Helvetica, sans-serif'
export const colors = {
textDark: '#1b222c',
textMuted: '#495365',
background: '#f4f5f6',
ctaGreen: '#098842',
ctaText: '#ffffff',
linkGreen: '#1e6b41',
linkHover: '#155a30',
logoGreen: '#04652f',
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

@@ -275,13 +275,11 @@ describe('EmailBuilder', function () {
describe('HTML email', function () {
it('should include a CTA button and a fallback CTA link', function (ctx) {
const dom = cheerio.load(ctx.email.html)
const buttonLink = dom('a:contains("Leave Feedback")')
const buttonLink = dom('a:contains("Leave feedback")')
expect(buttonLink.length).to.equal(1)
expect(buttonLink.attr('href')).to.equal(ctx.expectedUrl)
const fallback = dom('.force-overleaf-style').last()
expect(fallback.length).to.equal(1)
const fallbackLink = fallback.html()
expect(fallbackLink).to.contain(ctx.expectedUrl)
expect(ctx.email.html).to.contain('copy and paste this link')
expect(ctx.email.html).to.contain(ctx.expectedUrl)
})
})
@@ -317,10 +315,8 @@ describe('EmailBuilder', function () {
const buttonLink = dom('a:contains("Leave feedback")')
expect(buttonLink.length).to.equal(1)
expect(buttonLink.attr('href')).to.equal(ctx.expectedUrl)
const fallback = dom('.force-overleaf-style').last()
expect(fallback.length).to.equal(1)
const fallbackLink = fallback.html()
expect(fallbackLink).to.contain(ctx.expectedUrl)
expect(ctx.email.html).to.contain('copy and paste this link')
expect(ctx.email.html).to.contain(ctx.expectedUrl)
})
})
@@ -354,10 +350,8 @@ describe('EmailBuilder', function () {
const buttonLink = dom('a:contains("Confirm email")')
expect(buttonLink.length).to.equal(1)
expect(buttonLink.attr('href')).to.equal(ctx.opts.confirmEmailUrl)
const fallback = dom('.force-overleaf-style').last()
expect(fallback.length).to.equal(1)
const fallbackLink = fallback.html()
expect(fallbackLink).to.contain(ctx.opts.confirmEmailUrl)
expect(ctx.email.html).to.contain('copy and paste this link')
expect(ctx.email.html).to.contain(ctx.opts.confirmEmailUrl)
})
})
@@ -396,13 +390,11 @@ describe('EmailBuilder', function () {
describe('HTML email', function () {
it('should include a CTA button and a fallback CTA link', function (ctx) {
const dom = cheerio.load(ctx.email.html)
const buttonLink = dom('td a')
expect(buttonLink).to.exist
const buttonLink = dom('a:contains("View project")')
expect(buttonLink.length).to.equal(1)
expect(buttonLink.attr('href')).to.equal(ctx.expectedUrl)
const fallback = dom('.force-overleaf-style').last()
expect(fallback).to.exist
const fallbackLink = fallback.html().replace(/&amp;/g, '&')
expect(fallbackLink).to.contain(ctx.expectedUrl)
expect(ctx.email.html).to.contain('copy and paste this link')
expect(ctx.email.html).to.contain(ctx.expectedUrl)
})
})
@@ -438,13 +430,11 @@ describe('EmailBuilder', function () {
describe('HTML email', function () {
it('should include a CTA button and a fallback CTA link', function (ctx) {
const dom = cheerio.load(ctx.email.html)
const buttonLink = dom('td a')
expect(buttonLink).to.exist
const buttonLink = dom('a:contains("Reset password")')
expect(buttonLink.length).to.equal(1)
expect(buttonLink.attr('href')).to.equal(ctx.opts.setNewPasswordUrl)
const fallback = dom('.force-overleaf-style').last()
expect(fallback).to.exist
const fallbackLink = fallback.html().replace(/&amp;/g, '&')
expect(fallbackLink).to.contain(ctx.opts.setNewPasswordUrl)
expect(ctx.email.html).to.contain('copy and paste this link')
expect(ctx.email.html).to.contain(ctx.opts.setNewPasswordUrl)
})
})
@@ -475,13 +465,11 @@ describe('EmailBuilder', function () {
describe('HTML email', function () {
it('should include a CTA button and a fallback CTA link', function (ctx) {
const dom = cheerio.load(ctx.email.html)
const buttonLink = dom('a:contains("Reconfirm Email")')
const buttonLink = dom('a:contains("Reconfirm email")')
expect(buttonLink.length).to.equal(1)
expect(buttonLink.attr('href')).to.equal(ctx.opts.confirmEmailUrl)
const fallback = dom('.force-overleaf-style').last()
expect(fallback.length).to.equal(1)
const fallbackLink = fallback.html()
expect(fallbackLink).to.contain(ctx.opts.confirmEmailUrl)
expect(ctx.email.html).to.contain('copy and paste this link')
expect(ctx.email.html).to.contain(ctx.opts.confirmEmailUrl)
})
})
@@ -521,10 +509,8 @@ describe('EmailBuilder', function () {
const buttonLink = dom('a:contains("Join now")')
expect(buttonLink.length).to.equal(1)
expect(buttonLink.attr('href')).to.equal(ctx.opts.acceptInviteUrl)
const fallback = dom('.force-overleaf-style').last()
expect(fallback.length).to.equal(1)
const fallbackLink = fallback.html()
expect(fallbackLink).to.contain(ctx.opts.acceptInviteUrl)
expect(ctx.email.html).to.contain('copy and paste this link')
expect(ctx.email.html).to.contain(ctx.opts.acceptInviteUrl)
})
})
@@ -559,10 +545,8 @@ describe('EmailBuilder', function () {
const buttonLink = dom('a:contains("View Subscription Dashboard")')
expect(buttonLink.length).to.equal(1)
expect(buttonLink.attr('href')).to.equal(ctx.expectedUrl)
const fallback = dom('.force-overleaf-style').last()
expect(fallback.length).to.equal(1)
const fallbackLink = fallback.html()
expect(fallbackLink).to.contain(ctx.expectedUrl)
expect(ctx.email.html).to.contain('copy and paste this link')
expect(ctx.email.html).to.contain(ctx.expectedUrl)
})
})
@@ -593,10 +577,8 @@ describe('EmailBuilder', function () {
const buttonLink = dom(`a:contains("Open ${ctx.settings.appName}")`)
expect(buttonLink.length).to.equal(1)
expect(buttonLink.attr('href')).to.equal(ctx.settings.siteUrl)
const fallback = dom('.force-overleaf-style').last()
expect(fallback.length).to.equal(1)
const fallbackLink = fallback.html()
expect(fallbackLink).to.contain(ctx.settings.siteUrl)
expect(ctx.email.html).to.contain('copy and paste this link')
expect(ctx.email.html).to.contain(ctx.settings.siteUrl)
})
})
@@ -630,10 +612,8 @@ describe('EmailBuilder', function () {
const buttonLink = dom('a:contains("Set password")')
expect(buttonLink.length).to.equal(1)
expect(buttonLink.attr('href')).to.equal(ctx.opts.setNewPasswordUrl)
const fallback = dom('.force-overleaf-style').last()
expect(fallback.length).to.equal(1)
const fallbackLink = fallback.html().replace(/&amp;/, '&')
expect(fallbackLink).to.contain(ctx.opts.setNewPasswordUrl)
expect(ctx.email.html).to.contain('copy and paste this link')
expect(ctx.email.html).to.contain(ctx.opts.setNewPasswordUrl)
})
})
@@ -676,10 +656,8 @@ describe('EmailBuilder', function () {
const buttonLink = dom('a:contains("View project")')
expect(buttonLink.length).to.equal(1)
expect(buttonLink.attr('href')).to.equal(ctx.opts.inviteUrl)
const fallback = dom('.force-overleaf-style').last()
expect(fallback.length).to.equal(1)
const fallbackLink = fallback.html().replace(/&amp;/g, '&')
expect(fallbackLink).to.contain(ctx.opts.inviteUrl)
expect(ctx.email.html).to.contain('copy and paste this link')
expect(ctx.email.html).to.contain(ctx.opts.inviteUrl)
})
})
@@ -711,9 +689,8 @@ describe('EmailBuilder', function () {
const buttonLink = ctx.dom('a:contains("Confirm email")')
expect(buttonLink.length).to.equal(1)
expect(buttonLink.attr('href')).to.equal(ctx.opts.confirmEmailUrl)
const fallback = ctx.dom('.force-overleaf-style').last()
expect(fallback.length).to.equal(1)
expect(fallback.html()).to.contain(ctx.opts.confirmEmailUrl)
expect(ctx.email.html).to.contain('copy and paste this link')
expect(ctx.email.html).to.contain(ctx.opts.confirmEmailUrl)
})
it('should include help links', function (ctx) {
const helpGuidesLink = ctx.dom('a:contains("Help Guides")')
@@ -761,11 +738,12 @@ describe('EmailBuilder', function () {
expect(email.html).to.contain(
'You can still log in to Overleaf using one of our other'
)
const links = dom('a')
expect(links[0].attribs.href).to.equal(
const loginLink = dom('a:contains("login options")')
expect(loginLink.attr('href')).to.equal(
`${ctx.settings.siteUrl}/login`
)
expect(links[1].attribs.href).to.equal(setNewPasswordUrl)
const passwordLink = dom('a:contains("Set your new password")')
expect(passwordLink.attr('href')).to.equal(setNewPasswordUrl)
expect(email.html).to.contain(
"If you don't have a password, you can set one now."
)
@@ -813,8 +791,8 @@ describe('EmailBuilder', function () {
expect(email.html).to.contain(
'You now need an email address and password to sign in to your Overleaf account.'
)
const links = dom('a')
expect(links[0].attribs.href).to.equal(
const ctaLink = dom('a:contains("Set your new password")')
expect(ctaLink.attr('href')).to.equal(
`${ctx.settings.siteUrl}/user/password/reset`
)
@@ -1067,13 +1045,11 @@ describe('EmailBuilder', function () {
expect(ctx.email.html).to.contain(
'Remove inactive users to free up licenses'
)
const buttonLink = dom('td a')
const buttonLink = dom('a:contains("Add licenses")')
expect(buttonLink).to.exist
expect(buttonLink.attr('href')).to.equal(ctx.expectedUrl)
const fallback = dom('.force-overleaf-style').last()
expect(fallback).to.exist
const fallbackLink = fallback.html().replace(/&amp;/g, '&')
expect(fallbackLink).to.contain(ctx.expectedUrl)
expect(ctx.email.html).to.contain('copy and paste this link')
expect(ctx.email.html).to.contain(ctx.expectedUrl)
})
})
@@ -39,6 +39,27 @@ describe('EmailHandler', function () {
default: ctx.Queues,
}))
ctx.SplitTestHandler = {
promises: {
getAssignmentForUser: sinon.stub().resolves({ variant: 'default' }),
},
}
vi.doMock(
'../../../../app/src/Features/SplitTests/SplitTestHandler',
() => ({
default: ctx.SplitTestHandler,
})
)
ctx.UserGetter = {
promises: {
getUserByAnyEmail: sinon.stub().resolves(null),
},
}
vi.doMock('../../../../app/src/Features/User/UserGetter', () => ({
default: ctx.UserGetter,
}))
ctx.EmailHandler = (await import(MODULE_PATH)).default
})
@@ -0,0 +1,75 @@
import { vi, expect } from 'vitest'
import { load } from 'cheerio'
import path from 'node:path'
const LAYOUT_PATH = path.join(
import.meta.dirname,
'../../../../app/src/Features/Email/Layouts/UpdatedBaseEmailLayout'
)
async function buildLayoutWithEnv(env) {
vi.resetModules()
vi.doMock('@overleaf/settings', () => ({
default: {
appName: 'TestApp',
siteUrl: 'https://www.example.com',
env,
},
}))
const { default: layout } = await import(LAYOUT_PATH)
return layout({ body: 'Test body', footerMessage: '' })
}
describe('UpdatedBaseEmailLayout branding', function () {
afterEach(function () {
vi.restoreAllMocks()
vi.resetModules()
})
describe('on SaaS', function () {
it('should show the logo image', async function () {
const html = await buildLayoutWithEnv('saas')
const dom = load(html)
const logoImg = dom('img[alt="TestApp"]')
expect(logoImg.length).to.equal(1)
expect(logoImg.attr('src')).to.contain('email-logo@2x.png')
})
it('should show the tagline image', async function () {
const html = await buildLayoutWithEnv('saas')
expect(html).to.contain('email-footer-tagline@2x.png')
})
})
describe('on server-ce', function () {
it('should show the app name as text instead of logo', async function () {
const html = await buildLayoutWithEnv('server-ce')
const dom = load(html)
const logoImg = dom('img[alt="TestApp"]')
expect(logoImg.length).to.equal(0)
expect(html).to.contain('TestApp')
expect(html).to.not.contain('email-logo@2x.png')
})
it('should not show the tagline image', async function () {
const html = await buildLayoutWithEnv('server-ce')
expect(html).to.not.contain('email-footer-tagline@2x.png')
})
})
describe('on server-pro', function () {
it('should show the app name as text instead of logo', async function () {
const html = await buildLayoutWithEnv('server-pro')
const dom = load(html)
const logoImg = dom('img[alt="TestApp"]')
expect(logoImg.length).to.equal(0)
expect(html).to.contain('TestApp')
expect(html).to.not.contain('email-logo@2x.png')
})
it('should not show the tagline image', async function () {
const html = await buildLayoutWithEnv('server-pro')
expect(html).to.not.contain('email-footer-tagline@2x.png')
})
})
})