| 1 | import { readFileSync } from "node:fs"; |
| 2 | import { describe, expect, it } from "vitest"; |
| 3 | import { truncateChars } from "./truncate"; |
| 4 | |
| 5 | const webRoot = new URL("../", import.meta.url); |
| 6 | const ticker = readFileSync(new URL("components/ticker.tsx", webRoot), "utf8"); |
| 7 | const roadmap = readFileSync(new URL("lib/roadmap-feed.ts", webRoot), "utf8"); |
| 8 | |
| 9 | describe("truncateChars", () => { |
| 10 | it("leaves text at or under the limit untouched", () => { |
| 11 | expect(truncateChars("short", 70)).toBe("short"); |
| 12 | expect(truncateChars("x".repeat(70), 70)).toBe("x".repeat(70)); |
| 13 | }); |
| 14 | |
| 15 | it("never splits an astral character in half", () => { |
| 16 | // `"…🐋".slice(0, 70)` cuts between the surrogates and emits a lone |
| 17 | // U+D83D, which renders as U+FFFD next to the ellipsis. |
| 18 | const title = `${"x".repeat(69)}🐋 whale support`; |
| 19 | const cut = truncateChars(title, 70); |
| 20 | expect(cut).toBe(`${"x".repeat(69)}🐋…`); |
| 21 | // No surrogate survives that is not half of a well-formed pair. |
| 22 | const unpaired = cut.replace(/[\uD800-\uDBFF][\uDC00-\uDFFF]/g, ""); |
| 23 | expect(/[\uD800-\uDFFF]/.test(unpaired), "lone surrogate in output").toBe(false); |
| 24 | expect(title.slice(0, 70).charCodeAt(69)).toBe(0xd83d); |
| 25 | }); |
| 26 | |
| 27 | it("counts code points, not UTF-16 code units", () => { |
| 28 | // Forty whales are forty characters and eighty code units. The old |
| 29 | // `title.length > 70` test called this an 80-character title and cut it. |
| 30 | const whales = "🐋".repeat(40); |
| 31 | expect(whales.length).toBe(80); |
| 32 | expect(truncateChars(whales, 70)).toBe(whales); |
| 33 | }); |
| 34 | |
| 35 | it("supports a keep budget below the limit", () => { |
| 36 | expect(truncateChars("y".repeat(139), 140, 137)).toBe("y".repeat(139)); |
| 37 | expect(truncateChars("y".repeat(141), 140, 137)).toBe(`${"y".repeat(137)}…`); |
| 38 | }); |
| 39 | |
| 40 | it("is the one truncation rule the GitHub-fed surfaces use", () => { |
| 41 | expect(ticker).toContain("truncateChars(title, 70)"); |
| 42 | expect(ticker).not.toContain("title.slice("); |
| 43 | expect(roadmap).toContain("truncateChars(stripped, 140, 137)"); |
| 44 | expect(roadmap).not.toContain("stripped.slice("); |
| 45 | }); |
| 46 | }); |
| 47 |