From f7f59e172e9a1ed22ed4812a0e75c2bd33c5f2a7 Mon Sep 17 00:00:00 2001 From: Amayyas Date: Wed, 15 Jul 2026 10:01:54 +0200 Subject: [PATCH] test(text): cover the card-name and blurb display helpers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit lib/text.ts runs on every card and had no tests. cardDisplayName is more than a helper — it IS the fix for #48 (two-word surnames vanishing from the card), and nothing guarded that fix, so a refactor could silently bring the bug back. deEmDash rewrites the archetype blurb on the card and the OG image. Pins the #48 regression directly (a two-word surname must render in full), plus the three fallbacks it balances — surname too long (> 13 chars), too many words (> 3), or a single unsplittable word kept whole — each nailed on both sides of its boundary (9/10 name length, 13/14 surname chars, 3/4 words) where the next off-by-one would hide. For deEmDash: first break → ":" and later breaks → ",", the "--" variant, spacing normalisation, and that dash-free copy is left untouched. Mutation-checked: reintroducing the #48 bug (surname = last word only) turns these red. --- tests/text.test.ts | 101 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 101 insertions(+) create mode 100644 tests/text.test.ts diff --git a/tests/text.test.ts b/tests/text.test.ts new file mode 100644 index 0000000..425dbe8 --- /dev/null +++ b/tests/text.test.ts @@ -0,0 +1,101 @@ +import { describe, expect, it } from "vitest"; +import { cardDisplayName, deEmDash } from "@/lib/text"; + +// Both helpers run at the display layer on every card. deEmDash rewrites the +// archetype blurb (card + OG image); cardDisplayName decides which part of a name +// fits on the card — and it exists *because* of a shipped bug (#48, two-word +// surnames vanishing), so these tests pin that fix in place and nail the length / +// word-count boundaries where the next off-by-one would hide. + +describe("deEmDash", () => { + it("turns the first em-dash break into a colon (reads as a definition)", () => { + expect(deEmDash("the magician — a polyglot working across many stacks")).toBe( + "the magician: a polyglot working across many stacks", + ); + }); + + it("turns later breaks into commas, keeping the first as a colon", () => { + expect(deEmDash("engine — box-to-box — daily driver")).toBe("engine: box-to-box, daily driver"); + }); + + it("handles the ASCII '--' variant the same way", () => { + expect(deEmDash("deep playmaker -- coordinates from the back")).toBe( + "deep playmaker: coordinates from the back", + ); + }); + + it("normalises the spacing around a dash (none, or lots)", () => { + expect(deEmDash("a—b")).toBe("a: b"); + expect(deEmDash("a — b")).toBe("a: b"); + }); + + it("leaves dash-free text untouched", () => { + expect(deEmDash("a prolific shipper whose output lands")).toBe("a prolific shipper whose output lands"); + }); + + it("trims the result", () => { + expect(deEmDash(" hello world ")).toBe("hello world"); + }); + + it("returns an empty string unchanged", () => { + expect(deEmDash("")).toBe(""); + }); +}); + +describe("cardDisplayName", () => { + it("shows a short name (≤ 9 chars) in full", () => { + expect(cardDisplayName("Linus")).toBe("Linus"); + expect(cardDisplayName("Torvalds")).toBe("Torvalds"); // 8 chars, single word + }); + + it("drops the first name and shows the surname when the full name is long", () => { + expect(cardDisplayName("Linus Torvalds")).toBe("Torvalds"); + expect(cardDisplayName("The Octocat")).toBe("Octocat"); + }); + + // The #48 regression: a two-word surname used to vanish. It must show in full. + it("keeps a multi-word surname that fits (the #48 fix)", () => { + expect(cardDisplayName("Guido van Rossum")).toBe("van Rossum"); + expect(cardDisplayName("Ada de Lovelace")).toBe("de Lovelace"); + }); + + it("falls back to the first name when the surname is too long (> 13 chars)", () => { + expect(cardDisplayName("John Featherstonehaugh")).toBe("John"); // surname 17 chars + }); + + it("falls back to the first name when the surname runs to too many words (> 3)", () => { + // surname "bb cc dd ee" is only 11 chars, so it's the WORD count (4 > 3) that + // trips the fallback, not the char limit — isolating that branch. + expect(cardDisplayName("Aa bb cc dd ee")).toBe("Aa"); + }); + + it("keeps a single long word whole — there's nothing to trim to", () => { + expect(cardDisplayName("Supercalifragilistic")).toBe("Supercalifragilistic"); + }); + + it("normalises surrounding and inner whitespace", () => { + expect(cardDisplayName(" Linus Torvalds ")).toBe("Torvalds"); + }); + + it("returns empty / blank input as an empty string", () => { + expect(cardDisplayName("")).toBe(""); + expect(cardDisplayName(" ")).toBe(""); + }); + + describe("boundaries", () => { + it("keeps the full name at exactly 9 chars, trims at 10", () => { + expect(cardDisplayName("Ab Cdefgh")).toBe("Ab Cdefgh"); // 9 chars → full + expect(cardDisplayName("Abc Cdefgh")).toBe("Cdefgh"); // 10 chars → surname + }); + + it("keeps a 13-char surname, drops to the first name at 14", () => { + expect(cardDisplayName("X aaaaaaaaaaaaa")).toBe("aaaaaaaaaaaaa"); // surname 13 → kept + expect(cardDisplayName("X aaaaaaaaaaaaaa")).toBe("X"); // surname 14 → first name + }); + + it("keeps a 3-word surname, drops to the first name at 4 words", () => { + expect(cardDisplayName("a bb cc dd")).toBe("bb cc dd"); // 3 words → kept + expect(cardDisplayName("a bb cc dd ee")).toBe("a"); // 4 words → first name + }); + }); +});