From aa6e164fe651c5ec44e4e8cf6e9c0035f4be2a2d Mon Sep 17 00:00:00 2001 From: qingfeng312 <86755530+qingfeng312@users.noreply.github.com> Date: Wed, 27 May 2026 17:30:02 +0800 Subject: [PATCH] test: lift utility coverage above target --- src/arrays.test.ts | 41 +++++++++++++++++++++++++++++++++++++ src/dates.test.ts | 44 ++++++++++++++++++++++++++++++++++++++++ src/money.test.ts | 41 +++++++++++++++++++++++++++++++++++++ src/strings.test.ts | 49 +++++++++++++++++++++++++++++++++++++++++---- 4 files changed, 171 insertions(+), 4 deletions(-) create mode 100644 src/arrays.test.ts create mode 100644 src/dates.test.ts create mode 100644 src/money.test.ts diff --git a/src/arrays.test.ts b/src/arrays.test.ts new file mode 100644 index 0000000..11b4956 --- /dev/null +++ b/src/arrays.test.ts @@ -0,0 +1,41 @@ +import { describe, expect, it } from "vitest"; +import { chunk, dedupe, groupBy } from "./arrays"; + +describe("chunk", () => { + it("splits an array into fixed-size chunks", () => { + expect(chunk([1, 2, 3, 4, 5], 2)).toEqual([[1, 2], [3, 4], [5]]); + }); + + it("returns an empty array for non-positive sizes", () => { + expect(chunk([1, 2, 3], 0)).toEqual([]); + expect(chunk([1, 2, 3], -1)).toEqual([]); + }); + + it("returns no chunks for an empty input", () => { + expect(chunk([], 3)).toEqual([]); + }); +}); + +describe("dedupe", () => { + it("keeps the first occurrence order", () => { + expect(dedupe(["a", "b", "a", "c", "b"])).toEqual(["a", "b", "c"]); + }); + + it("uses Set identity semantics for objects", () => { + const shared = { id: 1 }; + const other = { id: 1 }; + expect(dedupe([shared, other, shared])).toEqual([shared, other]); + }); +}); + +describe("groupBy", () => { + it("groups values by string keys", () => { + const result = groupBy(["apple", "apricot", "banana"], (x) => x[0]); + expect(result).toEqual({ a: ["apple", "apricot"], b: ["banana"] }); + }); + + it("groups values by numeric keys", () => { + const result = groupBy([1, 2, 3, 4], (x) => x % 2); + expect(result).toEqual({ 0: [2, 4], 1: [1, 3] }); + }); +}); diff --git a/src/dates.test.ts b/src/dates.test.ts new file mode 100644 index 0000000..f5fb6b6 --- /dev/null +++ b/src/dates.test.ts @@ -0,0 +1,44 @@ +import { describe, expect, it } from "vitest"; +import { addDays, daysBetween, isWeekend, startOfDayUTC } from "./dates"; + +describe("daysBetween", () => { + it("returns the absolute day distance", () => { + const a = new Date("2026-05-01T00:00:00Z"); + const b = new Date("2026-05-04T00:00:00Z"); + expect(daysBetween(a, b)).toBe(3); + expect(daysBetween(b, a)).toBe(3); + }); + + it("rounds partial days to the nearest whole day", () => { + const a = new Date("2026-05-01T00:00:00Z"); + const b = new Date("2026-05-02T13:00:00Z"); + expect(daysBetween(a, b)).toBe(2); + }); +}); + +describe("isWeekend", () => { + it("detects Saturday and Sunday", () => { + expect(isWeekend(new Date("2026-05-30T12:00:00Z"))).toBe(true); + expect(isWeekend(new Date("2026-05-31T12:00:00Z"))).toBe(true); + }); + + it("returns false for weekdays", () => { + expect(isWeekend(new Date("2026-05-27T12:00:00Z"))).toBe(false); + }); +}); + +describe("addDays", () => { + it("adds positive and negative day offsets without mutating input", () => { + const input = new Date("2026-05-27T10:30:00Z"); + expect(addDays(input, 2).toISOString()).toBe("2026-05-29T10:30:00.000Z"); + expect(addDays(input, -3).toISOString()).toBe("2026-05-24T10:30:00.000Z"); + expect(input.toISOString()).toBe("2026-05-27T10:30:00.000Z"); + }); +}); + +describe("startOfDayUTC", () => { + it("returns midnight UTC for the supplied date", () => { + const result = startOfDayUTC(new Date("2026-05-27T23:59:59Z")); + expect(result.toISOString()).toBe("2026-05-27T00:00:00.000Z"); + }); +}); diff --git a/src/money.test.ts b/src/money.test.ts new file mode 100644 index 0000000..b6ffe3d --- /dev/null +++ b/src/money.test.ts @@ -0,0 +1,41 @@ +import { describe, expect, it } from "vitest"; +import { formatCents, netPayoutCents, platformFeeCents, splitEvenly } from "./money"; + +describe("formatCents", () => { + it("formats USD and EUR with symbols", () => { + expect(formatCents(12345)).toBe("$123.45"); + expect(formatCents(12345, "EUR")).toBe("€123.45"); + }); + + it("falls back to the currency code for other currencies", () => { + expect(formatCents(987, "GBP")).toBe("GBP 9.87"); + }); +}); + +describe("platformFeeCents", () => { + it("rounds basis-point fees to whole cents", () => { + expect(platformFeeCents(999, 250)).toBe(25); + }); + + it("returns zero for non-positive gross amounts or rates", () => { + expect(platformFeeCents(0, 250)).toBe(0); + expect(platformFeeCents(1000, 0)).toBe(0); + }); +}); + +describe("netPayoutCents", () => { + it("subtracts the platform fee from the gross amount", () => { + expect(netPayoutCents(10_000, 250)).toBe(9_750); + }); +}); + +describe("splitEvenly", () => { + it("splits cents evenly and distributes the remainder first", () => { + expect(splitEvenly(10, 3)).toEqual([4, 3, 3]); + }); + + it("returns an empty split for non-positive participant counts", () => { + expect(splitEvenly(10, 0)).toEqual([]); + expect(splitEvenly(10, -2)).toEqual([]); + }); +}); diff --git a/src/strings.test.ts b/src/strings.test.ts index daeb1cc..ba1187d 100644 --- a/src/strings.test.ts +++ b/src/strings.test.ts @@ -1,7 +1,48 @@ -import { describe, it, expect } from "vitest"; -import { slugify } from "./strings"; +import { describe, expect, it } from "vitest"; +import { escapeHtml, slugify, titleCase, truncate } from "./strings"; + describe("slugify", () => { - it("lowercases and dashifies basic input", () => { - expect(slugify("Hello World")).toBe("hello-world"); + it("lowercases, trims, and dashifies basic input", () => { + expect(slugify(" Hello World ")).toBe("hello-world"); + }); + + it("removes punctuation and collapses repeated separators", () => { + expect(slugify("Ship fast!!! With---tests")).toBe("ship-fast-with-tests"); + }); +}); + +describe("truncate", () => { + it("returns short strings unchanged", () => { + expect(truncate("short", 10)).toBe("short"); + }); + + it("adds a suffix when the input exceeds max length", () => { + expect(truncate("abcdefgh", 6, "...")).toBe("abc..."); + }); + + it("handles non-positive max lengths", () => { + expect(truncate("abcdefgh", 0)).toBe(""); + }); + + it("does not slice below zero when suffix is longer than max", () => { + expect(truncate("abcdefgh", 2, "...")).toBe("..."); + }); +}); + +describe("titleCase", () => { + it("capitalizes words and normalizes casing", () => { + expect(titleCase("hello WORLD from TESTS")).toBe("Hello World From Tests"); + }); + + it("preserves spacing behavior from split and join", () => { + expect(titleCase(" hello world ")).toBe(" Hello World "); + }); +}); + +describe("escapeHtml", () => { + it("escapes all supported HTML-sensitive characters", () => { + expect(escapeHtml(`Tom & "Jerry" 's tail>`)).toBe( + "Tom & "Jerry" <cat>'s tail>", + ); }); });