diff --git a/package.json b/package.json index 39cc814..0e17732 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "type": "module", "scripts": { "test": "vitest run", - "test:coverage": "vitest run --coverage --coverage.reporter=text-summary --coverage.include='src/**/*.ts' --coverage.exclude='src/**/*.test.ts'" + "test:coverage": "vitest run --coverage --coverage.reporter=text-summary --coverage.include=\"src/**/*.ts\" --coverage.exclude=\"src/**/*.test.ts\"" }, "devDependencies": { "@vitest/coverage-v8": "^2.0.0", diff --git a/src/arrays.test.ts b/src/arrays.test.ts new file mode 100644 index 0000000..df86bfb --- /dev/null +++ b/src/arrays.test.ts @@ -0,0 +1,36 @@ +import { describe, expect, it } from "vitest"; +import { chunk, dedupe, groupBy } from "./arrays"; + +describe("array utilities", () => { + it("splits arrays into fixed-size chunks and keeps a short final chunk", () => { + expect(chunk([1, 2, 3, 4, 5], 2)).toEqual([[1, 2], [3, 4], [5]]); + }); + + it("returns an empty chunk list for non-positive chunk sizes", () => { + expect(chunk([1, 2, 3], 0)).toEqual([]); + expect(chunk([1, 2, 3], -1)).toEqual([]); + }); + + it("deduplicates values while preserving first-seen order", () => { + expect(dedupe(["api", "web", "api", "cli"])).toEqual(["api", "web", "cli"]); + }); + + it("groups items by the provided key function", () => { + const grouped = groupBy( + [ + { owner: "alice", task: "docs" }, + { owner: "bob", task: "tests" }, + { owner: "alice", task: "review" }, + ], + (item) => item.owner, + ); + + expect(grouped).toEqual({ + alice: [ + { owner: "alice", task: "docs" }, + { owner: "alice", task: "review" }, + ], + bob: [{ owner: "bob", task: "tests" }], + }); + }); +}); \ No newline at end of file diff --git a/src/dates.test.ts b/src/dates.test.ts new file mode 100644 index 0000000..e0364bb --- /dev/null +++ b/src/dates.test.ts @@ -0,0 +1,31 @@ +import { describe, expect, it } from "vitest"; +import { addDays, daysBetween, isWeekend, startOfDayUTC } from "./dates"; + +describe("date utilities", () => { + it("returns the absolute rounded day distance between dates", () => { + const start = new Date("2026-05-01T12:00:00Z"); + const end = new Date("2026-05-04T11:00:00Z"); + + expect(daysBetween(start, end)).toBe(3); + expect(daysBetween(end, start)).toBe(3); + }); + + it("detects weekend and weekday dates", () => { + expect(isWeekend(new Date("2026-05-30T12:00:00Z"))).toBe(true); + expect(isWeekend(new Date("2026-06-01T12:00:00Z"))).toBe(false); + }); + + it("adds days without mutating the original date", () => { + const original = new Date("2026-05-31T10:00:00Z"); + const result = addDays(original, 2); + + expect(result.toISOString()).toBe("2026-06-02T10:00:00.000Z"); + expect(original.toISOString()).toBe("2026-05-31T10:00:00.000Z"); + }); + + it("normalizes a date to the start of its UTC day", () => { + expect(startOfDayUTC(new Date("2026-05-31T23:59:59Z")).toISOString()).toBe( + "2026-05-31T00:00:00.000Z", + ); + }); +}); \ No newline at end of file diff --git a/src/money.test.ts b/src/money.test.ts new file mode 100644 index 0000000..16a04d4 --- /dev/null +++ b/src/money.test.ts @@ -0,0 +1,35 @@ +import { describe, expect, it } from "vitest"; +import { formatCents, netPayoutCents, platformFeeCents, splitEvenly } from "./money"; + +describe("money utilities", () => { + it("formats cents as USD by default", () => { + expect(formatCents(1234)).toBe("$12.34"); + }); + + it("formats non-USD currencies with the amount preserved", () => { + expect(formatCents(1234, "EUR")).toContain("12.34"); + expect(formatCents(1234, "ZEC")).toBe("ZEC 12.34"); + }); + + it("calculates platform fees from basis points", () => { + expect(platformFeeCents(10_00, 250)).toBe(25); + expect(platformFeeCents(999, 333)).toBe(33); + }); + + it("does not charge fees for non-positive gross amounts or fee rates", () => { + expect(platformFeeCents(0, 250)).toBe(0); + expect(platformFeeCents(10_00, 0)).toBe(0); + }); + + it("subtracts the platform fee from the gross payout", () => { + expect(netPayoutCents(10_00, 2000)).toBe(800); + }); + + it("splits cents evenly and distributes the remainder to the first recipients", () => { + expect(splitEvenly(10, 3)).toEqual([4, 3, 3]); + }); + + it("returns no payouts when recipient count is not positive", () => { + expect(splitEvenly(10, 0)).toEqual([]); + }); +}); \ No newline at end of file diff --git a/src/strings.test.ts b/src/strings.test.ts index daeb1cc..61d0851 100644 --- a/src/strings.test.ts +++ b/src/strings.test.ts @@ -1,7 +1,47 @@ -import { describe, it, expect } from "vitest"; -import { slugify } from "./strings"; -describe("slugify", () => { - it("lowercases and dashifies basic input", () => { - expect(slugify("Hello World")).toBe("hello-world"); +import { describe, expect, it } from "vitest"; +import { escapeHtml, slugify, titleCase, truncate } from "./strings"; + +describe("string utilities", () => { + describe("slugify", () => { + it("lowercases and dashifies basic input", () => { + expect(slugify("Hello World")).toBe("hello-world"); + }); + + it("removes punctuation and trims repeated dashes", () => { + expect(slugify(" TaskBounty: Coverage ++ Uplift!!! ")).toBe("taskbounty-coverage-uplift"); + }); }); -}); + + describe("truncate", () => { + it("returns an empty string for non-positive maximum lengths", () => { + expect(truncate("coverage", 0)).toBe(""); + expect(truncate("coverage", -1)).toBe(""); + }); + + it("leaves strings within the maximum length unchanged", () => { + expect(truncate("short", 10)).toBe("short"); + }); + + it("shortens long strings and appends the configured suffix", () => { + expect(truncate("sandbox verified", 10, "...")).toBe("sandbox..."); + }); + }); + + describe("titleCase", () => { + it("capitalizes words and lowercases the remaining letters", () => { + expect(titleCase("mIXed CASE words")).toBe("Mixed Case Words"); + }); + + it("collapses repeated whitespace while title-casing words", () => { + expect(titleCase("Agent Memory")).toBe("Agent Memory"); + }); + }); + + describe("escapeHtml", () => { + it("escapes special HTML characters", () => { + expect(escapeHtml("&")).toBe( + "<span title='"bounty"'>&</span>", + ); + }); + }); +}); \ No newline at end of file