Files
mintel.me/apps/web/src/domain/email.spec.ts
T
mmintelandClaude Opus 5.5 390936856c
Build & Deploy / 🔍 Prepare (push) Successful in 5s
Build & Deploy / 🏗️ Build (push) Successful in 6m26s
Build & Deploy / 🚀 Deploy (push) Successful in 18s
Build & Deploy / 🩺 Smoke Test (push) Successful in 5s
Build & Deploy / 🔔 Notify (push) Successful in 2s
feat: message starter in the contact section
The visitor picks what they need and a prepared email types itself out; one
press opens it in their own mail app with subject and body filled in. The
mailto link is built by a tested pure function (src/domain/email.ts, spec
first) and only in the browser, so the address still never appears in the
page source.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 09:01:03 +02:00

172 lines
5.5 KiB
TypeScript

import { describe, expect, it } from "vitest";
import {
assembleEmail,
mailtoHref,
parseEmailParts,
spellOutEmail,
} from "./email";
const parts = { user: "hello", domain: "mintel", tld: "me" } as const;
describe("parseEmailParts", () => {
it("accepts a plain user, domain and top-level domain", () => {
expect(parseEmailParts(parts)).toEqual({ ok: true, value: parts });
});
it("accepts dots, dashes, plus signs and digits in the user part", () => {
const raw = { user: "marc.m-1+web", domain: "mintel", tld: "me" };
expect(parseEmailParts(raw)).toEqual({ ok: true, value: raw });
});
it("accepts a domain with a subdomain and dashes", () => {
const raw = { user: "hello", domain: "mail.my-site", tld: "co" };
expect(parseEmailParts(raw)).toEqual({ ok: true, value: raw });
});
it.each([
["an empty user", ""],
["an at sign in the user", "he@llo"],
["a space in the user", "he llo"],
["a leading dot", ".hello"],
["a trailing dot", "hello."],
["a missing user", undefined],
["a number instead of text", 42],
])("rejects %s as invalid-user", (_label, user) => {
expect(parseEmailParts({ user, domain: "mintel", tld: "me" })).toEqual({
ok: false,
error: "invalid-user",
});
});
it.each([
["an empty domain", ""],
["an at sign in the domain", "min@tel"],
["a space in the domain", "min tel"],
["a leading dash", "-mintel"],
["a trailing dash", "mintel-"],
["a leading dot", ".mintel"],
["a trailing dot", "mintel."],
["an underscore", "min_tel"],
["a missing domain", undefined],
])("rejects %s as invalid-domain", (_label, domain) => {
expect(parseEmailParts({ user: "hello", domain, tld: "me" })).toEqual({
ok: false,
error: "invalid-domain",
});
});
it.each([
["an empty top-level domain", ""],
["a single letter", "m"],
["digits", "m3"],
["a dot inside", "co.uk"],
["a leading dot", ".me"],
["a missing top-level domain", undefined],
])("rejects %s as invalid-tld", (_label, tld) => {
expect(parseEmailParts({ user: "hello", domain: "mintel", tld })).toEqual({
ok: false,
error: "invalid-tld",
});
});
it("accepts a two-letter and a longer top-level domain", () => {
expect(parseEmailParts({ ...parts, tld: "me" }).ok).toBe(true);
expect(parseEmailParts({ ...parts, tld: "digital" }).ok).toBe(true);
});
it("rejects input that is not an object as invalid-user", () => {
expect(parseEmailParts(null)).toEqual({ ok: false, error: "invalid-user" });
expect(parseEmailParts("hello@mintel.me")).toEqual({
ok: false,
error: "invalid-user",
});
});
it("reports the user first when several parts are wrong", () => {
expect(parseEmailParts({ user: "", domain: "", tld: "" })).toEqual({
ok: false,
error: "invalid-user",
});
});
it("reports the domain before the top-level domain", () => {
expect(parseEmailParts({ user: "hello", domain: "", tld: "" })).toEqual({
ok: false,
error: "invalid-domain",
});
});
});
describe("assembleEmail", () => {
it("joins the parts into the real address", () => {
expect(assembleEmail(parts)).toBe("hello@mintel.me");
});
it("keeps subdomains", () => {
expect(
assembleEmail({ user: "a.b", domain: "mail.site", tld: "org" }),
).toBe("a.b@mail.site.org");
});
});
describe("spellOutEmail", () => {
it("writes the address without an at sign or a dot before the ending", () => {
expect(spellOutEmail(parts)).toBe("hello [at] mintel [dot] me");
});
it("never contains the real address", () => {
expect(spellOutEmail(parts)).not.toContain("@");
expect(spellOutEmail(parts)).not.toContain(assembleEmail(parts));
});
it("spells out dots inside the domain as well", () => {
expect(
spellOutEmail({ user: "hello", domain: "mail.site", tld: "org" }),
).toBe("hello [at] mail [dot] site [dot] org");
});
});
describe("mailtoHref", () => {
it("opens a message to the address with subject and body", () => {
expect(mailtoHref(parts, { subject: "Hello", body: "Hi" })).toBe(
"mailto:hello@mintel.me?subject=Hello&body=Hi",
);
});
it("encodes spaces as %20, not as plus signs", () => {
expect(mailtoHref(parts, { subject: "A website", body: "Hi Marc" })).toBe(
"mailto:hello@mintel.me?subject=A%20website&body=Hi%20Marc",
);
});
it("encodes line breaks so the mail app keeps the paragraphs", () => {
expect(mailtoHref(parts, { subject: "S", body: "one\n\ntwo" })).toBe(
"mailto:hello@mintel.me?subject=S&body=one%0D%0A%0D%0Atwo",
);
});
it("encodes characters that would break the link", () => {
expect(mailtoHref(parts, { subject: "Q&A: 50%?", body: "a=b #1" })).toBe(
"mailto:hello@mintel.me?subject=Q%26A%3A%2050%25%3F&body=a%3Db%20%231",
);
});
it("keeps umlauts readable for the mail app", () => {
expect(mailtoHref(parts, { subject: "Grüße", body: "ä" })).toBe(
"mailto:hello@mintel.me?subject=Gr%C3%BC%C3%9Fe&body=%C3%A4",
);
});
it("leaves out an empty subject or body instead of sending empty fields", () => {
expect(mailtoHref(parts, { subject: "", body: "" })).toBe(
"mailto:hello@mintel.me",
);
expect(mailtoHref(parts, { subject: "Only subject", body: "" })).toBe(
"mailto:hello@mintel.me?subject=Only%20subject",
);
expect(mailtoHref(parts, { subject: "", body: "Only body" })).toBe(
"mailto:hello@mintel.me?body=Only%20body",
);
});
});