203 lines
8.9 KiB
TypeScript
203 lines
8.9 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import { defaultBrandTokens } from "@hivegrid/brand";
|
|
import EmailTemplates from "./EmailTemplates";
|
|
import type { AppConfiguration } from "~/utils/db/appConfiguration.db.server";
|
|
|
|
const appConfiguration = {
|
|
app: { name: "Acme Portal" },
|
|
email: { fromName: "Acme Support", supportEmail: "help@acme.test" },
|
|
branding: { logo: "https://cdn.acme.test/logo.png", favicon: "https://cdn.acme.test/favicon.ico" },
|
|
} as AppConfiguration;
|
|
|
|
const actionUrl = "https://app.acme.test/action";
|
|
|
|
describe("EmailTemplates", () => {
|
|
it("keeps the exported template registry stable", () => {
|
|
expect(EmailTemplates.allTemplates.map((template) => template.name)).toEqual([
|
|
"welcome",
|
|
"password-reset",
|
|
"user-invitation",
|
|
"email-verification",
|
|
"account-setup",
|
|
]);
|
|
});
|
|
|
|
it("renders welcome emails through the React-Email pipeline", () => {
|
|
const rendered = EmailTemplates.WELCOME_EMAIL.parse({
|
|
appConfiguration,
|
|
data: { name: "Ada", action_url: actionUrl },
|
|
locale: "en",
|
|
});
|
|
|
|
expect(rendered.subject).toBe("Welcome to Acme Portal, Ada");
|
|
expect(rendered.body).toContain(actionUrl);
|
|
expect(rendered.body).toContain("Acme Portal");
|
|
expect(rendered.body).toContain('<html lang="en">');
|
|
expect(rendered.body.startsWith("<!DOCTYPE html>")).toBe(true);
|
|
expect(rendered.text).toContain("Great to have you here, Ada");
|
|
expect(rendered.text).toContain("Booked features unlock after payment is confirmed");
|
|
expect(rendered.text).toContain("Continue setup");
|
|
expect(rendered.data).toEqual({ name: "Ada", action_url: actionUrl });
|
|
});
|
|
|
|
it("renders password reset emails with brand-aware subject", () => {
|
|
const rendered = EmailTemplates.PASSWORD_RESET_EMAIL.parse({
|
|
appConfiguration,
|
|
data: { name: "Ada", action_url: actionUrl },
|
|
});
|
|
|
|
expect(rendered.subject).toBe("Reset your Acme Portal password");
|
|
expect(rendered.body).toContain(actionUrl);
|
|
expect(rendered.body).toContain("Acme Portal");
|
|
});
|
|
|
|
it("renders user invitation emails with sender and organization data", () => {
|
|
const rendered = EmailTemplates.USER_INVITATION_EMAIL.parse({
|
|
appConfiguration,
|
|
data: {
|
|
name: "Ada",
|
|
invite_sender_name: "Grace",
|
|
invite_sender_organization: "Example Org",
|
|
action_url: actionUrl,
|
|
},
|
|
});
|
|
|
|
expect(rendered.subject).toBe("Grace invited you to Example Org in Acme Portal");
|
|
expect(rendered.body).toContain("Grace");
|
|
expect(rendered.body).toContain("Example Org");
|
|
expect(rendered.body).toContain(actionUrl);
|
|
});
|
|
|
|
it("renders email verification emails via the magic-link template", () => {
|
|
const rendered = EmailTemplates.VERIFICATION_EMAIL.parse({
|
|
appConfiguration,
|
|
data: { name: "Ada", action_url: actionUrl },
|
|
});
|
|
|
|
expect(rendered.subject).toBe("Please confirm your email address");
|
|
expect(rendered.body).toContain(actionUrl);
|
|
expect(rendered.text).toContain("review your plan, add-ons, and payment method");
|
|
expect(rendered.text).toContain("Features unlock only after payment is confirmed");
|
|
expect(rendered.body).toContain("This confirmation link is valid for 24 hours");
|
|
});
|
|
|
|
it("uses localized verification preview, expiry, outro and footer without English fallbacks", () => {
|
|
const translations: Record<string, string> = {
|
|
"emailVerification.subject": "Bestätige deine E-Mail-Adresse bei Acme Portal",
|
|
"emailVerification.previewText": "E-Mail-Adresse bei Acme Portal bestätigen",
|
|
"emailVerification.eyebrow": "E-Mail-Bestätigung",
|
|
"emailVerification.title_named": "Danke fürs Anmelden, Ada!",
|
|
"emailVerification.intro": "Bestätige deine E-Mail-Adresse, um die Einrichtung abzuschließen.",
|
|
"emailVerification.actionLabel": "E-Mail bestätigen",
|
|
"emailVerification.outro": "Wenn du diese Bestätigung nicht angefordert hast, kannst du diese E-Mail ignorieren.",
|
|
"emailVerification.expiresText": "Dieser Bestätigungslink ist 24 Stunden gültig und kann nur einmal verwendet werden.",
|
|
"common.signoff.closing": "Viele Grüße",
|
|
"common.signoff.team": "Dein Acme-Team",
|
|
"common.legal.privacyLabel": "Datenschutz",
|
|
"common.legal.termsLabel": "AGB",
|
|
"common.legal.imprintLabel": "Impressum",
|
|
"common.legal.unsubscribeLabel": "Abmelden",
|
|
"common.support.htmlBefore": "Bei Fragen kannst du uns gerne",
|
|
"common.support.emailLinkLabel": "eine E-Mail schreiben",
|
|
"common.support.htmlAfter": ".",
|
|
};
|
|
const rendered = EmailTemplates.VERIFICATION_EMAIL.parse({
|
|
appConfiguration,
|
|
data: { name: "Ada", action_url: actionUrl },
|
|
t: (key) => translations[key] ?? key,
|
|
locale: "de",
|
|
});
|
|
|
|
expect(rendered.body).toContain("Dieser Bestätigungslink ist 24 Stunden gültig");
|
|
expect(rendered.body).toContain("Wenn du diese Bestätigung nicht angefordert hast");
|
|
expect(rendered.body).toContain("Datenschutz");
|
|
expect(rendered.body).toContain('<html lang="de">');
|
|
expect(rendered.body).not.toContain("This confirmation link");
|
|
expect(rendered.body).not.toContain("If you did not request");
|
|
});
|
|
|
|
it.each(["pl", "ru", "uk", "hu"] as const)("sets the React email document language to %s", (locale) => {
|
|
const rendered = EmailTemplates.WELCOME_EMAIL.parse({
|
|
appConfiguration,
|
|
data: { name: "Ada", action_url: actionUrl },
|
|
t: (key) => key,
|
|
locale,
|
|
});
|
|
|
|
expect(rendered.body).toContain(`<html lang="${locale}">`);
|
|
});
|
|
|
|
it("renders account setup emails via the welcome template with plan-aware copy", () => {
|
|
const rendered = EmailTemplates.ACCOUNT_SETUP_EMAIL.parse({
|
|
appConfiguration,
|
|
data: { email: "ada@example.com", plan: "Pro", action_url: actionUrl },
|
|
});
|
|
|
|
expect(rendered.subject).toBe("Finish setting up your Acme Portal account");
|
|
expect(rendered.body).toContain("Pro access");
|
|
expect(rendered.body).toContain("ada@example.com");
|
|
expect(rendered.body).toContain(actionUrl);
|
|
});
|
|
|
|
it("falls back to default brand tokens when app configuration omits brand values", () => {
|
|
const rendered = EmailTemplates.WELCOME_EMAIL.parse({
|
|
appConfiguration: {} as AppConfiguration,
|
|
data: { action_url: actionUrl },
|
|
});
|
|
|
|
expect(rendered.subject).toBe(`Welcome to ${defaultBrandTokens.productName}`);
|
|
expect(rendered.body).toContain(defaultBrandTokens.productName);
|
|
});
|
|
|
|
it("uses the optional translator for subject and body chrome", () => {
|
|
const baseline = EmailTemplates.WELCOME_EMAIL.parse({
|
|
appConfiguration,
|
|
data: { name: "Ada", action_url: actionUrl },
|
|
});
|
|
const withTranslator = EmailTemplates.WELCOME_EMAIL.parse({
|
|
appConfiguration,
|
|
data: { name: "Ada", action_url: actionUrl },
|
|
t: (key, vars) => {
|
|
const values: Record<string, string> = {
|
|
"welcome.subject_named": `Willkommen bei ${vars?.productName}, ${vars?.name}!`,
|
|
"welcome.eyebrow": "Willkommen",
|
|
"welcome.title_named": `Hallo ${vars?.name}!`,
|
|
"welcome.intro": `Danke fuer ${vars?.productName}.`,
|
|
"welcome.actionLabel": "Einloggen",
|
|
"common.signoff.closing": "Viele Grüße",
|
|
"common.signoff.team": "Dein Acme-Team",
|
|
"common.legal.privacyLabel": "Datenschutz",
|
|
"common.legal.termsLabel": "AGB",
|
|
"common.legal.imprintLabel": "Impressum",
|
|
"common.legal.unsubscribeLabel": "Abmelden",
|
|
"common.support.htmlBefore": "Bei Fragen kannst du uns gerne",
|
|
"common.support.emailLinkLabel": "eine E-Mail schreiben",
|
|
"common.support.htmlAfter": ".",
|
|
};
|
|
return values[key] ?? key;
|
|
},
|
|
locale: "de",
|
|
});
|
|
expect(withTranslator.subject).toBe("Willkommen bei Acme Portal, Ada!");
|
|
expect(withTranslator.body).toContain("Hallo Ada!");
|
|
expect(withTranslator.body).toContain("Einloggen");
|
|
expect(withTranslator.body).toContain("Viele Grüße");
|
|
expect(withTranslator.body).toContain("Dein Acme-Team");
|
|
expect(withTranslator.body).toContain('<html lang="de">');
|
|
expect(withTranslator.body).toContain("Datenschutz");
|
|
expect(withTranslator.body).toContain("Bei Fragen kannst du uns gerne");
|
|
expect(withTranslator.body).toContain("eine E-Mail schreiben");
|
|
expect(withTranslator.body).not.toMatch(/<br\s*\/?>-<br\s*\/?>/u);
|
|
expect(withTranslator.text).toContain("Hallo Ada!");
|
|
expect(withTranslator.text).toContain("Datenschutz");
|
|
expect(withTranslator.text).not.toContain("\n-\n");
|
|
expect(withTranslator.body).not.toEqual(baseline.body);
|
|
});
|
|
|
|
it("localizes registry descriptions when a mail translator is available", () => {
|
|
const t = (key: string) => (key === "registry.welcomeDescription" ? "Lokalisierte Willkommensbeschreibung" : key);
|
|
expect(EmailTemplates.WELCOME_EMAIL.describe(t)).toBe("Lokalisierte Willkommensbeschreibung");
|
|
expect(EmailTemplates.WELCOME_EMAIL.description).toBe("Welcome email with setup link");
|
|
});
|
|
});
|