The site now presents one freelance developer on one page: hero, introduction, four typical cases (developer for a product, make an app reliable, automate a process, a website) with what you get, how it runs and what it costs, a process automation section, technology and project fit, how I work, two live references, questions and contact. - Hero and closing headline are drawn from running binary digits. - Each case explains its four steps with an animated drawing; the automation section shows example workflows with work travelling between systems; a pipeline run shows what a test catches before a release. - Contact is one email address, kept in parts and assembled in the browser so it never appears in the page source (src/domain/email.ts, spec first). - Imprint and privacy pages added. - The old site (blog, case studies, contact form, fixed-price pages, video rendering and related scripts) is removed. Its URLs redirect to the homepage. - The deploy smoke test checks the legal pages and the redirects instead of the removed case study. - Dev scripts use docker compose v2. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
123 lines
3.9 KiB
TypeScript
123 lines
3.9 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import { assembleEmail, 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");
|
|
});
|
|
});
|