test: 添加 Utils 纯函数单元测试 (测试计划 02)

覆盖 xml, hash, stringUtils, semver, uuid, format, frontmatterParser,
file, glob, diff 共 10 个模块的纯函数测试。
json.ts 因模块加载链路过重暂跳过。
共 190 个测试用例(含已有 array/set)全部通过。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
claude-code-best
2026-04-01 22:03:02 +08:00
parent 67baea3c7f
commit cad6409bfe
10 changed files with 935 additions and 0 deletions

View File

@@ -0,0 +1,77 @@
import { describe, expect, test } from "bun:test";
import { adjustHunkLineNumbers, getPatchFromContents } from "../diff";
describe("adjustHunkLineNumbers", () => {
test("shifts hunk line numbers by offset", () => {
const hunks = [
{ oldStart: 1, oldLines: 3, newStart: 1, newLines: 4, lines: [" a", "-b", "+c", "+d", " e"] },
] as any[];
const result = adjustHunkLineNumbers(hunks, 10);
expect(result[0].oldStart).toBe(11);
expect(result[0].newStart).toBe(11);
});
test("returns original hunks for zero offset", () => {
const hunks = [
{ oldStart: 5, oldLines: 2, newStart: 5, newLines: 2, lines: [] },
] as any[];
const result = adjustHunkLineNumbers(hunks, 0);
expect(result).toBe(hunks); // same reference
});
test("handles negative offset", () => {
const hunks = [
{ oldStart: 10, oldLines: 2, newStart: 10, newLines: 2, lines: [] },
] as any[];
const result = adjustHunkLineNumbers(hunks, -5);
expect(result[0].oldStart).toBe(5);
expect(result[0].newStart).toBe(5);
});
test("handles empty hunks array", () => {
expect(adjustHunkLineNumbers([], 10)).toEqual([]);
});
});
describe("getPatchFromContents", () => {
test("returns hunks for different content", () => {
const hunks = getPatchFromContents({
filePath: "test.txt",
oldContent: "hello\nworld",
newContent: "hello\nplanet",
});
expect(hunks.length).toBeGreaterThan(0);
expect(hunks[0].lines.some((l: string) => l.startsWith("-"))).toBe(true);
expect(hunks[0].lines.some((l: string) => l.startsWith("+"))).toBe(true);
});
test("returns empty hunks for identical content", () => {
const hunks = getPatchFromContents({
filePath: "test.txt",
oldContent: "same content",
newContent: "same content",
});
expect(hunks).toEqual([]);
});
test("handles content with ampersands", () => {
const hunks = getPatchFromContents({
filePath: "test.txt",
oldContent: "a & b",
newContent: "a & c",
});
expect(hunks.length).toBeGreaterThan(0);
// Verify ampersands are unescaped in the output
const allLines = hunks.flatMap((h: any) => h.lines);
expect(allLines.some((l: string) => l.includes("&"))).toBe(true);
});
test("handles empty old content (new file)", () => {
const hunks = getPatchFromContents({
filePath: "test.txt",
oldContent: "",
newContent: "new content",
});
expect(hunks.length).toBeGreaterThan(0);
});
});

View File

@@ -0,0 +1,95 @@
import { describe, expect, test } from "bun:test";
import {
convertLeadingTabsToSpaces,
addLineNumbers,
stripLineNumberPrefix,
pathsEqual,
normalizePathForComparison,
} from "../file";
describe("convertLeadingTabsToSpaces", () => {
test("converts leading tabs to 2 spaces each", () => {
expect(convertLeadingTabsToSpaces("\t\thello")).toBe(" hello");
});
test("only converts leading tabs", () => {
expect(convertLeadingTabsToSpaces("\thello\tworld")).toBe(" hello\tworld");
});
test("returns unchanged if no tabs", () => {
expect(convertLeadingTabsToSpaces("no tabs")).toBe("no tabs");
});
test("handles empty string", () => {
expect(convertLeadingTabsToSpaces("")).toBe("");
});
test("handles multiline content", () => {
const input = "\tline1\n\t\tline2\nline3";
const expected = " line1\n line2\nline3";
expect(convertLeadingTabsToSpaces(input)).toBe(expected);
});
});
describe("addLineNumbers", () => {
test("adds line numbers starting from 1", () => {
const result = addLineNumbers({ content: "a\nb\nc", startLine: 1 });
expect(result).toContain("1");
expect(result).toContain("a");
expect(result).toContain("b");
expect(result).toContain("c");
});
test("returns empty string for empty content", () => {
expect(addLineNumbers({ content: "", startLine: 1 })).toBe("");
});
test("respects startLine offset", () => {
const result = addLineNumbers({ content: "hello", startLine: 10 });
expect(result).toContain("10");
});
});
describe("stripLineNumberPrefix", () => {
test("strips arrow-separated prefix", () => {
expect(stripLineNumberPrefix(" 1→content")).toBe("content");
});
test("strips tab-separated prefix", () => {
expect(stripLineNumberPrefix("1\tcontent")).toBe("content");
});
test("returns line unchanged if no prefix", () => {
expect(stripLineNumberPrefix("no prefix")).toBe("no prefix");
});
test("handles large line numbers", () => {
expect(stripLineNumberPrefix("123456→content")).toBe("content");
});
});
describe("normalizePathForComparison", () => {
test("normalizes redundant separators", () => {
const result = normalizePathForComparison("/a//b/c");
expect(result).toBe("/a/b/c");
});
test("resolves dot segments", () => {
const result = normalizePathForComparison("/a/./b/../c");
expect(result).toBe("/a/c");
});
});
describe("pathsEqual", () => {
test("returns true for identical paths", () => {
expect(pathsEqual("/a/b/c", "/a/b/c")).toBe(true);
});
test("returns true for equivalent paths with dot segments", () => {
expect(pathsEqual("/a/./b", "/a/b")).toBe(true);
});
test("returns false for different paths", () => {
expect(pathsEqual("/a/b", "/a/c")).toBe(false);
});
});

View File

@@ -0,0 +1,133 @@
import { describe, expect, test } from "bun:test";
import {
formatFileSize,
formatSecondsShort,
formatDuration,
formatNumber,
formatTokens,
formatRelativeTime,
} from "../format";
describe("formatFileSize", () => {
test("formats bytes", () => {
expect(formatFileSize(500)).toBe("500 bytes");
});
test("formats kilobytes", () => {
expect(formatFileSize(1536)).toBe("1.5KB");
});
test("formats megabytes", () => {
expect(formatFileSize(1.5 * 1024 * 1024)).toBe("1.5MB");
});
test("formats gigabytes", () => {
expect(formatFileSize(2 * 1024 * 1024 * 1024)).toBe("2GB");
});
test("removes trailing .0", () => {
expect(formatFileSize(1024)).toBe("1KB");
});
});
describe("formatSecondsShort", () => {
test("formats milliseconds to seconds", () => {
expect(formatSecondsShort(1234)).toBe("1.2s");
});
test("formats zero", () => {
expect(formatSecondsShort(0)).toBe("0.0s");
});
test("formats sub-second", () => {
expect(formatSecondsShort(500)).toBe("0.5s");
});
});
describe("formatDuration", () => {
test("formats 0 as 0s", () => {
expect(formatDuration(0)).toBe("0s");
});
test("formats seconds", () => {
expect(formatDuration(5000)).toBe("5s");
});
test("formats minutes and seconds", () => {
expect(formatDuration(125000)).toBe("2m 5s");
});
test("formats hours", () => {
expect(formatDuration(3661000)).toBe("1h 1m 1s");
});
test("formats days", () => {
expect(formatDuration(90000000)).toBe("1d 1h 0m");
});
test("hideTrailingZeros removes zero components", () => {
expect(formatDuration(3600000, { hideTrailingZeros: true })).toBe("1h");
expect(formatDuration(60000, { hideTrailingZeros: true })).toBe("1m");
});
test("mostSignificantOnly returns largest unit", () => {
expect(formatDuration(90000000, { mostSignificantOnly: true })).toBe("1d");
expect(formatDuration(3661000, { mostSignificantOnly: true })).toBe("1h");
});
});
describe("formatNumber", () => {
test("formats small numbers as-is", () => {
expect(formatNumber(900)).toBe("900");
});
test("formats thousands with k suffix", () => {
const result = formatNumber(1321);
expect(result).toContain("k");
});
test("formats millions", () => {
const result = formatNumber(1500000);
expect(result).toContain("m");
});
});
describe("formatTokens", () => {
test("removes .0 from formatted number", () => {
const result = formatTokens(1000);
expect(result).not.toContain(".0");
});
test("formats small numbers", () => {
expect(formatTokens(500)).toBe("500");
});
});
describe("formatRelativeTime", () => {
const now = new Date("2026-01-15T12:00:00Z");
test("formats seconds ago", () => {
const date = new Date("2026-01-15T11:59:30Z");
const result = formatRelativeTime(date, { now });
expect(result).toContain("30");
expect(result).toContain("ago");
});
test("formats minutes ago", () => {
const date = new Date("2026-01-15T11:55:00Z");
const result = formatRelativeTime(date, { now });
expect(result).toContain("5");
expect(result).toContain("ago");
});
test("formats future time", () => {
const date = new Date("2026-01-15T13:00:00Z");
const result = formatRelativeTime(date, { now });
expect(result).toContain("in");
});
test("handles zero difference", () => {
const result = formatRelativeTime(now, { now });
expect(result).toContain("0");
});
});

View File

@@ -0,0 +1,164 @@
import { describe, expect, test } from "bun:test";
import {
parseFrontmatter,
splitPathInFrontmatter,
parsePositiveIntFromFrontmatter,
parseBooleanFrontmatter,
parseShellFrontmatter,
} from "../frontmatterParser";
describe("parseFrontmatter", () => {
test("parses valid frontmatter", () => {
const md = `---
description: A test
type: user
---
Content here`;
const result = parseFrontmatter(md);
expect(result.frontmatter.description).toBe("A test");
expect(result.frontmatter.type).toBe("user");
expect(result.content).toBe("Content here");
});
test("returns empty frontmatter when none exists", () => {
const md = "Just content, no frontmatter";
const result = parseFrontmatter(md);
expect(result.frontmatter).toEqual({});
expect(result.content).toBe(md);
});
test("handles empty frontmatter block", () => {
const md = `---
---
Content`;
const result = parseFrontmatter(md);
expect(result.frontmatter).toEqual({});
expect(result.content).toBe("Content");
});
test("handles frontmatter with list values", () => {
const md = `---
allowed-tools:
- Bash
- Read
---
Content`;
const result = parseFrontmatter(md);
expect(result.frontmatter["allowed-tools"]).toEqual(["Bash", "Read"]);
});
});
describe("splitPathInFrontmatter", () => {
test("splits comma-separated paths", () => {
expect(splitPathInFrontmatter("a, b, c")).toEqual(["a", "b", "c"]);
});
test("expands brace patterns", () => {
expect(splitPathInFrontmatter("src/*.{ts,tsx}")).toEqual([
"src/*.ts",
"src/*.tsx",
]);
});
test("handles nested brace expansion", () => {
expect(splitPathInFrontmatter("{a,b}/{c,d}")).toEqual([
"a/c", "a/d", "b/c", "b/d",
]);
});
test("handles array input", () => {
expect(splitPathInFrontmatter(["a", "b"])).toEqual(["a", "b"]);
});
test("returns empty array for non-string", () => {
expect(splitPathInFrontmatter(123 as any)).toEqual([]);
});
test("preserves braces in comma-separated list", () => {
expect(splitPathInFrontmatter("a, src/*.{ts,tsx}")).toEqual([
"a",
"src/*.ts",
"src/*.tsx",
]);
});
});
describe("parsePositiveIntFromFrontmatter", () => {
test("returns number for positive integer", () => {
expect(parsePositiveIntFromFrontmatter(5)).toBe(5);
});
test("parses string number", () => {
expect(parsePositiveIntFromFrontmatter("10")).toBe(10);
});
test("returns undefined for zero", () => {
expect(parsePositiveIntFromFrontmatter(0)).toBeUndefined();
});
test("returns undefined for negative number", () => {
expect(parsePositiveIntFromFrontmatter(-1)).toBeUndefined();
});
test("returns undefined for float", () => {
expect(parsePositiveIntFromFrontmatter(1.5)).toBeUndefined();
});
test("returns undefined for null/undefined", () => {
expect(parsePositiveIntFromFrontmatter(null)).toBeUndefined();
expect(parsePositiveIntFromFrontmatter(undefined)).toBeUndefined();
});
test("returns undefined for non-numeric string", () => {
expect(parsePositiveIntFromFrontmatter("abc")).toBeUndefined();
});
});
describe("parseBooleanFrontmatter", () => {
test("returns true for boolean true", () => {
expect(parseBooleanFrontmatter(true)).toBe(true);
});
test("returns true for string 'true'", () => {
expect(parseBooleanFrontmatter("true")).toBe(true);
});
test("returns false for boolean false", () => {
expect(parseBooleanFrontmatter(false)).toBe(false);
});
test("returns false for string 'false'", () => {
expect(parseBooleanFrontmatter("false")).toBe(false);
});
test("returns false for null/undefined", () => {
expect(parseBooleanFrontmatter(null)).toBe(false);
expect(parseBooleanFrontmatter(undefined)).toBe(false);
});
});
describe("parseShellFrontmatter", () => {
test("returns bash for 'bash'", () => {
expect(parseShellFrontmatter("bash", "test")).toBe("bash");
});
test("returns powershell for 'powershell'", () => {
expect(parseShellFrontmatter("powershell", "test")).toBe("powershell");
});
test("returns undefined for null", () => {
expect(parseShellFrontmatter(null, "test")).toBeUndefined();
});
test("returns undefined for unrecognized value", () => {
expect(parseShellFrontmatter("zsh", "test")).toBeUndefined();
});
test("is case insensitive", () => {
expect(parseShellFrontmatter("BASH", "test")).toBe("bash");
});
test("returns undefined for empty string", () => {
expect(parseShellFrontmatter("", "test")).toBeUndefined();
});
});

View File

@@ -0,0 +1,40 @@
import { describe, expect, test } from "bun:test";
import { extractGlobBaseDirectory } from "../glob";
describe("extractGlobBaseDirectory", () => {
test("extracts base dir from glob with *", () => {
const result = extractGlobBaseDirectory("src/utils/*.ts");
expect(result.baseDir).toBe("src/utils");
expect(result.relativePattern).toBe("*.ts");
});
test("extracts base dir from glob with **", () => {
const result = extractGlobBaseDirectory("src/**/*.ts");
expect(result.baseDir).toBe("src");
expect(result.relativePattern).toBe("**/*.ts");
});
test("returns dirname for literal path", () => {
const result = extractGlobBaseDirectory("src/utils/file.ts");
expect(result.baseDir).toBe("src/utils");
expect(result.relativePattern).toBe("file.ts");
});
test("handles glob starting with pattern", () => {
const result = extractGlobBaseDirectory("*.ts");
expect(result.baseDir).toBe("");
expect(result.relativePattern).toBe("*.ts");
});
test("handles braces pattern", () => {
const result = extractGlobBaseDirectory("src/{a,b}/*.ts");
expect(result.baseDir).toBe("src");
expect(result.relativePattern).toBe("{a,b}/*.ts");
});
test("handles question mark pattern", () => {
const result = extractGlobBaseDirectory("src/?.ts");
expect(result.baseDir).toBe("src");
expect(result.relativePattern).toBe("?.ts");
});
});

View File

@@ -0,0 +1,57 @@
import { describe, expect, test } from "bun:test";
import { djb2Hash, hashContent, hashPair } from "../hash";
describe("djb2Hash", () => {
test("returns a number", () => {
expect(typeof djb2Hash("hello")).toBe("number");
});
test("returns 0 for empty string", () => {
expect(djb2Hash("")).toBe(0);
});
test("is deterministic", () => {
expect(djb2Hash("test")).toBe(djb2Hash("test"));
});
test("different strings produce different hashes", () => {
expect(djb2Hash("abc")).not.toBe(djb2Hash("def"));
});
test("returns 32-bit integer", () => {
const hash = djb2Hash("some long string to hash");
expect(hash).toBe(hash | 0); // bitwise OR with 0 preserves 32-bit int
});
});
describe("hashContent", () => {
test("returns a string", () => {
expect(typeof hashContent("hello")).toBe("string");
});
test("is deterministic", () => {
expect(hashContent("test")).toBe(hashContent("test"));
});
test("different strings produce different hashes", () => {
expect(hashContent("abc")).not.toBe(hashContent("def"));
});
});
describe("hashPair", () => {
test("returns a string", () => {
expect(typeof hashPair("a", "b")).toBe("string");
});
test("is deterministic", () => {
expect(hashPair("a", "b")).toBe(hashPair("a", "b"));
});
test("order matters", () => {
expect(hashPair("a", "b")).not.toBe(hashPair("b", "a"));
});
test("disambiguates different splits", () => {
expect(hashPair("ts", "code")).not.toBe(hashPair("tsc", "ode"));
});
});

View File

@@ -0,0 +1,98 @@
import { describe, expect, test } from "bun:test";
import { gt, gte, lt, lte, satisfies, order } from "../semver";
describe("gt", () => {
test("returns true when a > b", () => {
expect(gt("2.0.0", "1.0.0")).toBe(true);
});
test("returns false when a < b", () => {
expect(gt("1.0.0", "2.0.0")).toBe(false);
});
test("returns false when equal", () => {
expect(gt("1.0.0", "1.0.0")).toBe(false);
});
});
describe("gte", () => {
test("returns true when a > b", () => {
expect(gte("2.0.0", "1.0.0")).toBe(true);
});
test("returns true when equal", () => {
expect(gte("1.0.0", "1.0.0")).toBe(true);
});
test("returns false when a < b", () => {
expect(gte("1.0.0", "2.0.0")).toBe(false);
});
});
describe("lt", () => {
test("returns true when a < b", () => {
expect(lt("1.0.0", "2.0.0")).toBe(true);
});
test("returns false when a > b", () => {
expect(lt("2.0.0", "1.0.0")).toBe(false);
});
test("returns false when equal", () => {
expect(lt("1.0.0", "1.0.0")).toBe(false);
});
});
describe("lte", () => {
test("returns true when a < b", () => {
expect(lte("1.0.0", "2.0.0")).toBe(true);
});
test("returns true when equal", () => {
expect(lte("1.0.0", "1.0.0")).toBe(true);
});
test("returns false when a > b", () => {
expect(lte("2.0.0", "1.0.0")).toBe(false);
});
});
describe("satisfies", () => {
test("matches exact version", () => {
expect(satisfies("1.2.3", "1.2.3")).toBe(true);
});
test("matches range", () => {
expect(satisfies("1.2.3", ">=1.0.0")).toBe(true);
});
test("does not match out-of-range version", () => {
expect(satisfies("0.9.0", ">=1.0.0")).toBe(false);
});
test("matches caret range", () => {
expect(satisfies("1.2.3", "^1.0.0")).toBe(true);
});
test("does not match major bump in caret", () => {
expect(satisfies("2.0.0", "^1.0.0")).toBe(false);
});
});
describe("order", () => {
test("returns 1 when a > b", () => {
expect(order("2.0.0", "1.0.0")).toBe(1);
});
test("returns -1 when a < b", () => {
expect(order("1.0.0", "2.0.0")).toBe(-1);
});
test("returns 0 when equal", () => {
expect(order("1.0.0", "1.0.0")).toBe(0);
});
test("compares patch versions", () => {
expect(order("1.0.1", "1.0.0")).toBe(1);
});
});

View File

@@ -0,0 +1,195 @@
import { describe, expect, test } from "bun:test";
import {
escapeRegExp,
capitalize,
plural,
firstLineOf,
countCharInString,
normalizeFullWidthDigits,
normalizeFullWidthSpace,
safeJoinLines,
EndTruncatingAccumulator,
truncateToLines,
} from "../stringUtils";
describe("escapeRegExp", () => {
test("escapes special regex chars", () => {
expect(escapeRegExp("a.b*c?d")).toBe("a\\.b\\*c\\?d");
});
test("escapes brackets and parens", () => {
expect(escapeRegExp("[foo](bar)")).toBe("\\[foo\\]\\(bar\\)");
});
test("escapes all special chars", () => {
expect(escapeRegExp("^${}()|[]\\.*+?")).toBe(
"\\^\\$\\{\\}\\(\\)\\|\\[\\]\\\\\\.\\*\\+\\?"
);
});
test("returns normal string unchanged", () => {
expect(escapeRegExp("hello")).toBe("hello");
});
});
describe("capitalize", () => {
test("uppercases first char", () => {
expect(capitalize("hello")).toBe("Hello");
});
test("does NOT lowercase rest", () => {
expect(capitalize("fooBar")).toBe("FooBar");
});
test("handles single char", () => {
expect(capitalize("a")).toBe("A");
});
test("handles empty string", () => {
expect(capitalize("")).toBe("");
});
});
describe("plural", () => {
test("returns singular for 1", () => {
expect(plural(1, "file")).toBe("file");
});
test("returns plural for 0", () => {
expect(plural(0, "file")).toBe("files");
});
test("returns plural for many", () => {
expect(plural(3, "file")).toBe("files");
});
test("uses custom plural form", () => {
expect(plural(2, "entry", "entries")).toBe("entries");
});
});
describe("firstLineOf", () => {
test("returns first line of multiline string", () => {
expect(firstLineOf("line1\nline2\nline3")).toBe("line1");
});
test("returns whole string if no newline", () => {
expect(firstLineOf("single line")).toBe("single line");
});
test("returns empty string for leading newline", () => {
expect(firstLineOf("\nline2")).toBe("");
});
});
describe("countCharInString", () => {
test("counts occurrences of a character", () => {
expect(countCharInString("hello world", "l")).toBe(3);
});
test("returns 0 for no match", () => {
expect(countCharInString("hello", "z")).toBe(0);
});
test("counts from start offset", () => {
expect(countCharInString("aabaa", "a", 2)).toBe(2);
});
test("returns 0 for empty string", () => {
expect(countCharInString("", "a")).toBe(0);
});
});
describe("normalizeFullWidthDigits", () => {
test("converts full-width digits to half-width", () => {
expect(normalizeFullWidthDigits("")).toBe("0123456789");
});
test("leaves half-width digits unchanged", () => {
expect(normalizeFullWidthDigits("0123")).toBe("0123");
});
test("handles mixed content", () => {
expect(normalizeFullWidthDigits("test")).toBe("test123");
});
});
describe("normalizeFullWidthSpace", () => {
test("converts full-width space to half-width", () => {
expect(normalizeFullWidthSpace("a\u3000b")).toBe("a b");
});
test("leaves normal spaces unchanged", () => {
expect(normalizeFullWidthSpace("a b")).toBe("a b");
});
});
describe("safeJoinLines", () => {
test("joins lines with delimiter", () => {
expect(safeJoinLines(["a", "b", "c"], ",")).toBe("a,b,c");
});
test("truncates when exceeding maxSize", () => {
const result = safeJoinLines(["hello", "world", "foo"], ",", 12);
expect(result.length).toBeLessThanOrEqual(12 + "...[truncated]".length);
expect(result).toContain("...[truncated]");
});
test("returns empty string for empty input", () => {
expect(safeJoinLines([])).toBe("");
});
});
describe("EndTruncatingAccumulator", () => {
test("accumulates text", () => {
const acc = new EndTruncatingAccumulator(100);
acc.append("hello ");
acc.append("world");
expect(acc.toString()).toBe("hello world");
});
test("truncates when exceeding maxSize", () => {
const acc = new EndTruncatingAccumulator(10);
acc.append("12345678901234567890");
expect(acc.truncated).toBe(true);
expect(acc.length).toBe(10);
});
test("reports total bytes received", () => {
const acc = new EndTruncatingAccumulator(5);
acc.append("1234567890");
expect(acc.totalBytes).toBe(10);
});
test("clear resets state", () => {
const acc = new EndTruncatingAccumulator(100);
acc.append("hello");
acc.clear();
expect(acc.toString()).toBe("");
expect(acc.length).toBe(0);
expect(acc.truncated).toBe(false);
});
test("stops accepting data once truncated and full", () => {
const acc = new EndTruncatingAccumulator(5);
acc.append("12345");
acc.append("67890");
expect(acc.length).toBe(5);
acc.append("more");
expect(acc.length).toBe(5);
});
});
describe("truncateToLines", () => {
test("returns text unchanged if within limit", () => {
expect(truncateToLines("a\nb\nc", 5)).toBe("a\nb\nc");
});
test("truncates text exceeding limit", () => {
expect(truncateToLines("a\nb\nc\nd\ne", 3)).toBe("a\nb\nc…");
});
test("handles single line", () => {
expect(truncateToLines("hello", 1)).toBe("hello");
});
});

View File

@@ -0,0 +1,34 @@
import { describe, expect, test } from "bun:test";
import { validateUuid } from "../uuid";
describe("validateUuid", () => {
test("validates correct UUID", () => {
const result = validateUuid("550e8400-e29b-41d4-a716-446655440000");
expect(result).toBe("550e8400-e29b-41d4-a716-446655440000");
});
test("validates uppercase UUID", () => {
const result = validateUuid("550E8400-E29B-41D4-A716-446655440000");
expect(result).not.toBeNull();
});
test("returns null for non-string", () => {
expect(validateUuid(123)).toBeNull();
expect(validateUuid(null)).toBeNull();
expect(validateUuid(undefined)).toBeNull();
});
test("returns null for invalid UUID format", () => {
expect(validateUuid("not-a-uuid")).toBeNull();
expect(validateUuid("550e8400-e29b-41d4-a716")).toBeNull();
expect(validateUuid("550e8400e29b41d4a716446655440000")).toBeNull();
});
test("returns null for empty string", () => {
expect(validateUuid("")).toBeNull();
});
test("returns null for UUID with invalid chars", () => {
expect(validateUuid("550e8400-e29b-41d4-a716-44665544000g")).toBeNull();
});
});

View File

@@ -0,0 +1,42 @@
import { describe, expect, test } from "bun:test";
import { escapeXml, escapeXmlAttr } from "../xml";
describe("escapeXml", () => {
test("escapes ampersand", () => {
expect(escapeXml("a & b")).toBe("a &amp; b");
});
test("escapes less-than", () => {
expect(escapeXml("<div>")).toBe("&lt;div&gt;");
});
test("escapes greater-than", () => {
expect(escapeXml("a > b")).toBe("a &gt; b");
});
test("escapes multiple special chars", () => {
expect(escapeXml("<a & b>")).toBe("&lt;a &amp; b&gt;");
});
test("returns empty string unchanged", () => {
expect(escapeXml("")).toBe("");
});
test("returns normal text unchanged", () => {
expect(escapeXml("hello world")).toBe("hello world");
});
});
describe("escapeXmlAttr", () => {
test("escapes double quotes", () => {
expect(escapeXmlAttr('say "hello"')).toBe("say &quot;hello&quot;");
});
test("escapes single quotes", () => {
expect(escapeXmlAttr("it's")).toBe("it&apos;s");
});
test("escapes all special chars", () => {
expect(escapeXmlAttr('<a & "b">')).toBe("&lt;a &amp; &quot;b&quot;&gt;");
});
});