Explorar o código

feat: 搭建单元测试基础设施 — Bun test runner + 示例测试

添加 bunfig.toml 配置、test script,以及三组示例测试:
- src/utils/array.ts (intersperse, count, uniq)
- src/utils/set.ts (difference, intersects, every, union)
- packages/color-diff-napi (ansi256FromRgb, colorToEscape, detectLanguage 等)

41 tests, 0 failures.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
claude-code-best hai 3 semanas
pai
achega
e443a8fa51

+ 1 - 1
TODO.md

@@ -21,5 +21,5 @@
 - [ ] 冗余代码检查
 - [x] git hook 的配置
 - [ ] 代码健康度检查
-- [ ] 单元测试基础设施搭建 (test runner 配置)
+- [x] 单元测试基础设施搭建 (test runner 配置)
 - [ ] CI/CD 流水线 (GitHub Actions)

+ 3 - 0
bunfig.toml

@@ -0,0 +1,3 @@
+[test]
+root = "."
+timeout = 10000

+ 2 - 1
package.json

@@ -16,7 +16,8 @@
         "lint": "biome check src/",
         "lint:fix": "biome check --fix src/",
         "format": "biome format --write src/",
-        "prepare": "git config core.hooksPath .githooks"
+        "prepare": "git config core.hooksPath .githooks",
+        "test": "bun test"
     },
     "dependencies": {
         "@alcalzone/ansi-tokenize": "^0.3.0",

+ 102 - 0
packages/color-diff-napi/src/__tests__/color-diff.test.ts

@@ -0,0 +1,102 @@
+import { describe, expect, test } from "bun:test";
+import { __test } from "../index";
+
+const { ansi256FromRgb, colorToEscape, detectColorMode, detectLanguage, tokenize } = __test;
+
+describe("ansi256FromRgb", () => {
+	test("black maps to index 16", () => {
+		expect(ansi256FromRgb(0, 0, 0)).toBe(16);
+	});
+
+	test("pure red maps to cube red", () => {
+		expect(ansi256FromRgb(255, 0, 0)).toBe(196);
+	});
+
+	test("pure green maps to cube green", () => {
+		expect(ansi256FromRgb(0, 255, 0)).toBe(46);
+	});
+
+	test("pure blue maps to cube blue", () => {
+		expect(ansi256FromRgb(0, 0, 255)).toBe(21);
+	});
+
+	test("grey values map to grey ramp", () => {
+		const idx = ansi256FromRgb(128, 128, 128);
+		// Should be in the grey ramp range (232-255)
+		expect(idx).toBeGreaterThanOrEqual(232);
+		expect(idx).toBeLessThanOrEqual(255);
+	});
+});
+
+describe("colorToEscape", () => {
+	test("palette index < 8 uses standard ANSI codes", () => {
+		const color = { r: 1, g: 0, b: 0, a: 0 }; // palette index 1
+		expect(colorToEscape(color, true, "truecolor")).toBe("\x1b[31m"); // fg red
+		expect(colorToEscape(color, false, "truecolor")).toBe("\x1b[41m"); // bg red
+	});
+
+	test("palette index 8-15 uses bright ANSI codes", () => {
+		const color = { r: 9, g: 0, b: 0, a: 0 }; // bright red
+		expect(colorToEscape(color, true, "truecolor")).toBe("\x1b[91m");
+	});
+
+	test("alpha=1 returns terminal default", () => {
+		const color = { r: 0, g: 0, b: 0, a: 1 };
+		expect(colorToEscape(color, true, "truecolor")).toBe("\x1b[39m");
+		expect(colorToEscape(color, false, "truecolor")).toBe("\x1b[49m");
+	});
+
+	test("truecolor uses RGB escape", () => {
+		const color = { r: 100, g: 150, b: 200, a: 255 };
+		expect(colorToEscape(color, true, "truecolor")).toBe("\x1b[38;2;100;150;200m");
+	});
+
+	test("color256 uses 256-color escape", () => {
+		const color = { r: 100, g: 150, b: 200, a: 255 };
+		const result = colorToEscape(color, true, "color256");
+		expect(result).toMatch(/^\x1b\[38;5;\d+m$/);
+	});
+});
+
+describe("detectColorMode", () => {
+	test("returns ansi for ansi-containing theme names", () => {
+		expect(detectColorMode("ansi")).toBe("ansi");
+		expect(detectColorMode("base16-ansi-dark")).toBe("ansi");
+	});
+
+	test("returns truecolor or color256 for non-ansi themes", () => {
+		const mode = detectColorMode("monokai");
+		expect(["truecolor", "color256"]).toContain(mode);
+	});
+});
+
+describe("detectLanguage", () => {
+	test("detects language from file extension", () => {
+		expect(detectLanguage("index.ts")).toBe("ts");
+		expect(detectLanguage("main.py")).toBe("py");
+		expect(detectLanguage("style.css")).toBe("css");
+	});
+
+	test("detects language from known filenames", () => {
+		expect(detectLanguage("Makefile")).toBe("makefile");
+		expect(detectLanguage("Dockerfile")).toBe("dockerfile");
+	});
+
+	test("returns null for unknown extensions", () => {
+		expect(detectLanguage("file.xyz123")).toBeNull();
+	});
+});
+
+describe("tokenize", () => {
+	test("returns array of tokens", () => {
+		const result = tokenize("hello world");
+		expect(Array.isArray(result)).toBe(true);
+		expect(result.length).toBeGreaterThan(0);
+	});
+
+	test("preserves original text when joined", () => {
+		const text = "foo bar baz";
+		const tokens = tokenize(text);
+		expect(tokens.join("")).toBe(text);
+	});
+});

+ 58 - 0
src/utils/__tests__/array.test.ts

@@ -0,0 +1,58 @@
+import { describe, expect, test } from "bun:test";
+import { count, intersperse, uniq } from "../array";
+
+describe("intersperse", () => {
+	test("inserts separator between elements", () => {
+		const result = intersperse([1, 2, 3], () => 0);
+		expect(result).toEqual([1, 0, 2, 0, 3]);
+	});
+
+	test("returns empty array for empty input", () => {
+		expect(intersperse([], () => 0)).toEqual([]);
+	});
+
+	test("returns single element without separator", () => {
+		expect(intersperse([1], () => 0)).toEqual([1]);
+	});
+
+	test("passes index to separator function", () => {
+		const result = intersperse(["a", "b", "c"], (i) => `sep-${i}`);
+		expect(result).toEqual(["a", "sep-1", "b", "sep-2", "c"]);
+	});
+});
+
+describe("count", () => {
+	test("counts matching elements", () => {
+		expect(count([1, 2, 3, 4, 5], (x) => x > 3)).toBe(2);
+	});
+
+	test("returns 0 for empty array", () => {
+		expect(count([], () => true)).toBe(0);
+	});
+
+	test("returns 0 when nothing matches", () => {
+		expect(count([1, 2, 3], (x) => x > 10)).toBe(0);
+	});
+
+	test("counts all when everything matches", () => {
+		expect(count([1, 2, 3], () => true)).toBe(3);
+	});
+});
+
+describe("uniq", () => {
+	test("removes duplicates", () => {
+		expect(uniq([1, 2, 2, 3, 3, 3])).toEqual([1, 2, 3]);
+	});
+
+	test("preserves order of first occurrence", () => {
+		expect(uniq([3, 1, 2, 1, 3])).toEqual([3, 1, 2]);
+	});
+
+	test("handles empty array", () => {
+		expect(uniq([])).toEqual([]);
+	});
+
+	test("works with strings", () => {
+		expect(uniq(["a", "b", "a"])).toEqual(["a", "b"]);
+	});
+});

+ 63 - 0
src/utils/__tests__/set.test.ts

@@ -0,0 +1,63 @@
+import { describe, expect, test } from "bun:test";
+import { difference, every, intersects, union } from "../set";
+
+describe("difference", () => {
+	test("returns elements in a but not in b", () => {
+		const result = difference(new Set([1, 2, 3]), new Set([2, 3, 4]));
+		expect(result).toEqual(new Set([1]));
+	});
+
+	test("returns empty set when a is subset of b", () => {
+		expect(difference(new Set([1, 2]), new Set([1, 2, 3]))).toEqual(new Set());
+	});
+
+	test("returns a when b is empty", () => {
+		expect(difference(new Set([1, 2]), new Set())).toEqual(new Set([1, 2]));
+	});
+});
+
+describe("intersects", () => {
+	test("returns true when sets share elements", () => {
+		expect(intersects(new Set([1, 2]), new Set([2, 3]))).toBe(true);
+	});
+
+	test("returns false when sets are disjoint", () => {
+		expect(intersects(new Set([1, 2]), new Set([3, 4]))).toBe(false);
+	});
+
+	test("returns false for empty sets", () => {
+		expect(intersects(new Set(), new Set([1]))).toBe(false);
+		expect(intersects(new Set([1]), new Set())).toBe(false);
+	});
+});
+
+describe("every", () => {
+	test("returns true when a is subset of b", () => {
+		expect(every(new Set([1, 2]), new Set([1, 2, 3]))).toBe(true);
+	});
+
+	test("returns false when a has elements not in b", () => {
+		expect(every(new Set([1, 4]), new Set([1, 2, 3]))).toBe(false);
+	});
+
+	test("returns true for empty a", () => {
+		expect(every(new Set(), new Set([1, 2]))).toBe(true);
+	});
+});
+
+describe("union", () => {
+	test("combines both sets", () => {
+		const result = union(new Set([1, 2]), new Set([3, 4]));
+		expect(result).toEqual(new Set([1, 2, 3, 4]));
+	});
+
+	test("deduplicates shared elements", () => {
+		const result = union(new Set([1, 2]), new Set([2, 3]));
+		expect(result).toEqual(new Set([1, 2, 3]));
+	});
+
+	test("handles empty sets", () => {
+		expect(union(new Set(), new Set([1]))).toEqual(new Set([1]));
+		expect(union(new Set([1]), new Set())).toEqual(new Set([1]));
+	});
+});