Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@
"description": "Small TypeScript utility library used as a TaskBounty Coverage Uplift dogfood subject. Intentionally low-coverage at ~30% to demonstrate a real lift to 80%.",
"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": "vitest run --coverage --coverage.reporter=lcov --coverage.reporter=json --coverage.reporter=text-summary --coverage.include='src/**/*.ts' --coverage.exclude='src/**/*.test.ts'",
"posttest": "ln -sf coverage/lcov.info lcov.info 2>/dev/null; ln -sf coverage/coverage-final.json coverage.json 2>/dev/null; echo 'Linked coverage files for TaskBounty sandbox compatibility'",
"test:coverage": "vitest run --coverage --coverage.reporter=lcov --coverage.reporter=json --coverage.reporter=text-summary --coverage.include='src/**/*.ts' --coverage.exclude='src/**/*.test.ts'"
},
"devDependencies": {
"@vitest/coverage-v8": "^2.0.0",
Expand Down
83 changes: 83 additions & 0 deletions src/arrays.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { describe, it, expect } from "vitest";
import { chunk, dedupe, groupBy } from "./arrays";

describe("chunk", () => {
it("splits array into chunks of given size", () => {
expect(chunk([1, 2, 3, 4, 5], 2)).toEqual([[1, 2], [3, 4], [5]]);
});

it("returns empty array for size <= 0", () => {
expect(chunk([1, 2, 3], 0)).toEqual([]);
expect(chunk([1, 2, 3], -1)).toEqual([]);
});

it("returns single chunk if size >= array length", () => {
expect(chunk([1, 2, 3], 10)).toEqual([[1, 2, 3]]);
});

it("handles empty array", () => {
expect(chunk([], 2)).toEqual([]);
});

it("works with exact division", () => {
expect(chunk([1, 2, 3, 4], 2)).toEqual([[1, 2], [3, 4]]);
});
});

describe("dedupe", () => {
it("removes duplicate numbers", () => {
expect(dedupe([1, 2, 2, 3, 1])).toEqual([1, 2, 3]);
});

it("removes duplicate strings", () => {
expect(dedupe(["a", "b", "a", "c"])).toEqual(["a", "b", "c"]);
});

it("returns same array if no duplicates", () => {
expect(dedupe([1, 2, 3])).toEqual([1, 2, 3]);
});

it("handles empty array", () => {
expect(dedupe([])).toEqual([]);
});

it("handles single element", () => {
expect(dedupe([42])).toEqual([42]);
});
});

describe("groupBy", () => {
it("groups by string key", () => {
const items = [
{ type: "fruit", name: "apple" },
{ type: "fruit", name: "banana" },
{ type: "veg", name: "carrot" },
];
const result = groupBy(items, (x) => x.type);
expect(result).toEqual({
fruit: [{ type: "fruit", name: "apple" }, { type: "fruit", name: "banana" }],
veg: [{ type: "veg", name: "carrot" }],
});
});

it("groups by numeric key", () => {
const items = [10, 15, 20, 25, 30];
const result = groupBy(items, (x) => (x >= 20 ? "high" : "low"));
expect(result).toEqual({
low: [10, 15],
high: [20, 25, 30],
});
});

it("handles empty array", () => {
expect(groupBy([], (x: number) => "a")).toEqual({});
});

it("preserves order within groups", () => {
const items = [3, 1, 2, 1, 3];
const result = groupBy(items, (x) => x);
expect(result[1]).toEqual([1, 1]);
expect(result[2]).toEqual([2]);
expect(result[3]).toEqual([3, 3]);
});
});
102 changes: 102 additions & 0 deletions src/dates.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import { describe, it, expect } from "vitest";
import { daysBetween, isWeekend, addDays, startOfDayUTC } from "./dates";

describe("daysBetween", () => {
it("calculates days between same date", () => {
const d = new Date("2024-01-01");
expect(daysBetween(d, d)).toBe(0);
});

it("calculates days between different dates", () => {
const a = new Date("2024-01-01T00:00:00Z");
const b = new Date("2024-01-10T00:00:00Z");
expect(daysBetween(a, b)).toBe(9);
});

it("always returns positive (uses Math.abs)", () => {
const a = new Date("2024-01-01");
const b = new Date("2024-01-10");
expect(daysBetween(b, a)).toBe(9);
});

it("rounds correctly for partial days", () => {
const a = new Date("2024-01-01T00:00:00Z");
const b = new Date("2024-01-01T12:00:00Z");
expect(daysBetween(a, b)).toBe(1);
});
});

describe("isWeekend", () => {
it("returns true for Saturday (day 6)", () => {
expect(isWeekend(new Date("2024-01-06T00:00:00Z"))).toBe(true);
});

it("returns true for Sunday (day 0)", () => {
expect(isWeekend(new Date("2024-01-07T00:00:00Z"))).toBe(true);
});

it("returns false for Monday", () => {
expect(isWeekend(new Date("2024-01-08T00:00:00Z"))).toBe(false);
});

it("returns false for Wednesday", () => {
expect(isWeekend(new Date("2024-01-10T00:00:00Z"))).toBe(false);
});

it("returns false for Friday", () => {
expect(isWeekend(new Date("2024-01-05T00:00:00Z"))).toBe(false);
});
});

describe("addDays", () => {
it("adds positive days", () => {
const d = new Date("2024-01-01");
const result = addDays(d, 5);
expect(result.getDate()).toBe(6);
});

it("adds negative days", () => {
const d = new Date("2024-01-10");
const result = addDays(d, -3);
expect(result.getDate()).toBe(7);
});

it("does not mutate original date", () => {
const d = new Date("2024-01-01");
addDays(d, 5);
expect(d.getDate()).toBe(1);
});

it("handles month boundary", () => {
const d = new Date("2024-01-30");
const result = addDays(d, 5);
expect(result.getMonth()).toBe(1); // February
expect(result.getDate()).toBe(4);
});

it("returns a new Date instance", () => {
const d = new Date("2024-01-01");
const result = addDays(d, 1);
expect(result).not.toBe(d);
});
});

describe("startOfDayUTC", () => {
it("truncates time to midnight UTC", () => {
const d = new Date("2024-01-15T14:30:45.123Z");
const result = startOfDayUTC(d);
expect(result.toISOString()).toBe("2024-01-15T00:00:00.000Z");
});

it("handles date already at midnight", () => {
const d = new Date("2024-01-15T00:00:00.000Z");
const result = startOfDayUTC(d);
expect(result.toISOString()).toBe("2024-01-15T00:00:00.000Z");
});

it("handles end of month", () => {
const d = new Date("2024-01-31T23:59:59.999Z");
const result = startOfDayUTC(d);
expect(result.toISOString()).toBe("2024-01-31T00:00:00.000Z");
});
});
86 changes: 86 additions & 0 deletions src/money.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { describe, it, expect } from "vitest";
import { formatCents, platformFeeCents, netPayoutCents, splitEvenly } from "./money";

describe("formatCents", () => {
it("formats USD cents with $", () => {
expect(formatCents(100)).toBe("$1.00");
});

it("formats EUR cents with €", () => {
expect(formatCents(150, "EUR")).toBe("€1.50");
});

it("formats other currencies with prefix", () => {
expect(formatCents(200, "GBP")).toBe("GBP 2.00");
});

it("handles zero", () => {
expect(formatCents(0)).toBe("$0.00");
});

it("handles large values", () => {
expect(formatCents(999999)).toBe("$9999.99");
});
});

describe("platformFeeCents", () => {
it("calculates fee correctly", () => {
// 1000 cents * 250 bps / 10000 = 25 cents
expect(platformFeeCents(1000, 250)).toBe(25);
});

it("returns 0 for grossCents <= 0", () => {
expect(platformFeeCents(0, 250)).toBe(0);
expect(platformFeeCents(-10, 250)).toBe(0);
});

it("returns 0 for bps <= 0", () => {
expect(platformFeeCents(1000, 0)).toBe(0);
expect(platformFeeCents(1000, -10)).toBe(0);
});

it("rounds to integer", () => {
// 99 * 12.5 bps = 1.2375 → Math.round = 1
expect(platformFeeCents(99, 125)).toBe(1);
});
});

describe("netPayoutCents", () => {
it("calculates net after fee", () => {
// 1000 - platformFeeCents(1000, 250) = 1000 - 25 = 975
expect(netPayoutCents(1000, 250)).toBe(975);
});

it("returns 0 for zero gross", () => {
expect(netPayoutCents(0, 250)).toBe(0);
});
});

describe("splitEvenly", () => {
it("splits evenly with no remainder", () => {
expect(splitEvenly(100, 5)).toEqual([20, 20, 20, 20, 20]);
});

it("distributes remainder to first N recipients", () => {
expect(splitEvenly(10, 3)).toEqual([4, 3, 3]);
});

it("returns empty array for n <= 0", () => {
expect(splitEvenly(100, 0)).toEqual([]);
expect(splitEvenly(100, -1)).toEqual([]);
});

it("handles single recipient", () => {
expect(splitEvenly(50, 1)).toEqual([50]);
});

it("handles all remainder", () => {
expect(splitEvenly(5, 5)).toEqual([1, 1, 1, 1, 1]);
});

it("sum of parts equals grossCents", () => {
const result = splitEvenly(100, 7);
const sum = result.reduce((a, b) => a + b, 0);
expect(sum).toBe(100);
});
});
106 changes: 105 additions & 1 deletion src/strings.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,111 @@
import { describe, it, expect } from "vitest";
import { slugify } from "./strings";
import { slugify, truncate, titleCase, escapeHtml } from "./strings";

describe("slugify", () => {
it("lowercases and dashifies basic input", () => {
expect(slugify("Hello World")).toBe("hello-world");
});

it("removes special characters", () => {
expect(slugify("Hello! @World#")).toBe("hello-world");
});

it("handles multiple spaces and dashes", () => {
expect(slugify(" Hello World ")).toBe("hello-world");
});

it("handles empty string", () => {
expect(slugify("")).toBe("");
});

it("handles already-dashed input", () => {
expect(slugify("hello-world-test")).toBe("hello-world-test");
});

it("trims leading and trailing dashes", () => {
expect(slugify("-hello-world-")).toBe("hello-world");
});
});

describe("truncate", () => {
it("returns full string if shorter than max", () => {
expect(truncate("hello", 10)).toBe("hello");
});

it("truncates with default suffix", () => {
expect(truncate("hello world this is long", 10)).toBe("hello wor…");
});

it("returns empty string for max <= 0", () => {
expect(truncate("hello", 0)).toBe("");
expect(truncate("hello", -1)).toBe("");
});

it("uses custom suffix", () => {
expect(truncate("hello world", 8, ">>>")).toBe("hello>>>");
});

it("handles max exactly equal to input length", () => {
expect(truncate("hello", 5)).toBe("hello");
});

it("handles empty input", () => {
expect(truncate("", 5)).toBe("");
});
});

describe("titleCase", () => {
it("capitalizes first letter of each word", () => {
expect(titleCase("hello world")).toBe("Hello World");
});

it("handles single word", () => {
expect(titleCase("hello")).toBe("Hello");
});

it("lowercases the rest of each word", () => {
expect(titleCase("HELLO WORLD")).toBe("Hello World");
});

it("handles mixed case", () => {
expect(titleCase("hElLo WoRlD")).toBe("Hello World");
});

it("handles empty string", () => {
expect(titleCase("")).toBe("");
});

it("collapses multiple spaces into one", () => {
expect(titleCase("hello world")).toBe("Hello World");
});
});

describe("escapeHtml", () => {
it("escapes &", () => {
expect(escapeHtml("a & b")).toBe("a &amp; b");
});

it("escapes < and >", () => {
expect(escapeHtml("<script>")).toBe("&lt;script&gt;");
});

it("escapes double quotes", () => {
expect(escapeHtml('say "hello"')).toBe("say &quot;hello&quot;");
});

it("escapes single quotes", () => {
expect(escapeHtml("it's")).toBe("it&#39;s");
});

it("handles string with no special chars", () => {
expect(escapeHtml("hello world")).toBe("hello world");
});

it("handles empty string", () => {
expect(escapeHtml("")).toBe("");
});

it("escapes all five characters together", () => {
expect(escapeHtml("<a href=\"x&y\">it's</a>")).toBe("&lt;a href=&quot;x&amp;y&quot;&gt;it&#39;s&lt;/a&gt;");
});
});