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", ); }); });