Skip to content

Commit e443a8f

Browse files
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>
1 parent 9dd1eef commit e443a8f

6 files changed

Lines changed: 229 additions & 2 deletions

File tree

TODO.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,5 +21,5 @@
2121
- [ ] 冗余代码检查
2222
- [x] git hook 的配置
2323
- [ ] 代码健康度检查
24-
- [ ] 单元测试基础设施搭建 (test runner 配置)
24+
- [x] 单元测试基础设施搭建 (test runner 配置)
2525
- [ ] CI/CD 流水线 (GitHub Actions)

bunfig.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
[test]
2+
root = "."
3+
timeout = 10000

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@
1616
"lint": "biome check src/",
1717
"lint:fix": "biome check --fix src/",
1818
"format": "biome format --write src/",
19-
"prepare": "git config core.hooksPath .githooks"
19+
"prepare": "git config core.hooksPath .githooks",
20+
"test": "bun test"
2021
},
2122
"dependencies": {
2223
"@alcalzone/ansi-tokenize": "^0.3.0",
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
import { describe, expect, test } from "bun:test";
2+
import { __test } from "../index";
3+
4+
const { ansi256FromRgb, colorToEscape, detectColorMode, detectLanguage, tokenize } = __test;
5+
6+
describe("ansi256FromRgb", () => {
7+
test("black maps to index 16", () => {
8+
expect(ansi256FromRgb(0, 0, 0)).toBe(16);
9+
});
10+
11+
test("pure red maps to cube red", () => {
12+
expect(ansi256FromRgb(255, 0, 0)).toBe(196);
13+
});
14+
15+
test("pure green maps to cube green", () => {
16+
expect(ansi256FromRgb(0, 255, 0)).toBe(46);
17+
});
18+
19+
test("pure blue maps to cube blue", () => {
20+
expect(ansi256FromRgb(0, 0, 255)).toBe(21);
21+
});
22+
23+
test("grey values map to grey ramp", () => {
24+
const idx = ansi256FromRgb(128, 128, 128);
25+
// Should be in the grey ramp range (232-255)
26+
expect(idx).toBeGreaterThanOrEqual(232);
27+
expect(idx).toBeLessThanOrEqual(255);
28+
});
29+
});
30+
31+
describe("colorToEscape", () => {
32+
test("palette index < 8 uses standard ANSI codes", () => {
33+
const color = { r: 1, g: 0, b: 0, a: 0 }; // palette index 1
34+
expect(colorToEscape(color, true, "truecolor")).toBe("\x1b[31m"); // fg red
35+
expect(colorToEscape(color, false, "truecolor")).toBe("\x1b[41m"); // bg red
36+
});
37+
38+
test("palette index 8-15 uses bright ANSI codes", () => {
39+
const color = { r: 9, g: 0, b: 0, a: 0 }; // bright red
40+
expect(colorToEscape(color, true, "truecolor")).toBe("\x1b[91m");
41+
});
42+
43+
test("alpha=1 returns terminal default", () => {
44+
const color = { r: 0, g: 0, b: 0, a: 1 };
45+
expect(colorToEscape(color, true, "truecolor")).toBe("\x1b[39m");
46+
expect(colorToEscape(color, false, "truecolor")).toBe("\x1b[49m");
47+
});
48+
49+
test("truecolor uses RGB escape", () => {
50+
const color = { r: 100, g: 150, b: 200, a: 255 };
51+
expect(colorToEscape(color, true, "truecolor")).toBe("\x1b[38;2;100;150;200m");
52+
});
53+
54+
test("color256 uses 256-color escape", () => {
55+
const color = { r: 100, g: 150, b: 200, a: 255 };
56+
const result = colorToEscape(color, true, "color256");
57+
expect(result).toMatch(/^\x1b\[38;5;\d+m$/);
58+
});
59+
});
60+
61+
describe("detectColorMode", () => {
62+
test("returns ansi for ansi-containing theme names", () => {
63+
expect(detectColorMode("ansi")).toBe("ansi");
64+
expect(detectColorMode("base16-ansi-dark")).toBe("ansi");
65+
});
66+
67+
test("returns truecolor or color256 for non-ansi themes", () => {
68+
const mode = detectColorMode("monokai");
69+
expect(["truecolor", "color256"]).toContain(mode);
70+
});
71+
});
72+
73+
describe("detectLanguage", () => {
74+
test("detects language from file extension", () => {
75+
expect(detectLanguage("index.ts")).toBe("ts");
76+
expect(detectLanguage("main.py")).toBe("py");
77+
expect(detectLanguage("style.css")).toBe("css");
78+
});
79+
80+
test("detects language from known filenames", () => {
81+
expect(detectLanguage("Makefile")).toBe("makefile");
82+
expect(detectLanguage("Dockerfile")).toBe("dockerfile");
83+
});
84+
85+
test("returns null for unknown extensions", () => {
86+
expect(detectLanguage("file.xyz123")).toBeNull();
87+
});
88+
});
89+
90+
describe("tokenize", () => {
91+
test("returns array of tokens", () => {
92+
const result = tokenize("hello world");
93+
expect(Array.isArray(result)).toBe(true);
94+
expect(result.length).toBeGreaterThan(0);
95+
});
96+
97+
test("preserves original text when joined", () => {
98+
const text = "foo bar baz";
99+
const tokens = tokenize(text);
100+
expect(tokens.join("")).toBe(text);
101+
});
102+
});

src/utils/__tests__/array.test.ts

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import { describe, expect, test } from "bun:test";
2+
import { count, intersperse, uniq } from "../array";
3+
4+
describe("intersperse", () => {
5+
test("inserts separator between elements", () => {
6+
const result = intersperse([1, 2, 3], () => 0);
7+
expect(result).toEqual([1, 0, 2, 0, 3]);
8+
});
9+
10+
test("returns empty array for empty input", () => {
11+
expect(intersperse([], () => 0)).toEqual([]);
12+
});
13+
14+
test("returns single element without separator", () => {
15+
expect(intersperse([1], () => 0)).toEqual([1]);
16+
});
17+
18+
test("passes index to separator function", () => {
19+
const result = intersperse(["a", "b", "c"], (i) => `sep-${i}`);
20+
expect(result).toEqual(["a", "sep-1", "b", "sep-2", "c"]);
21+
});
22+
});
23+
24+
describe("count", () => {
25+
test("counts matching elements", () => {
26+
expect(count([1, 2, 3, 4, 5], (x) => x > 3)).toBe(2);
27+
});
28+
29+
test("returns 0 for empty array", () => {
30+
expect(count([], () => true)).toBe(0);
31+
});
32+
33+
test("returns 0 when nothing matches", () => {
34+
expect(count([1, 2, 3], (x) => x > 10)).toBe(0);
35+
});
36+
37+
test("counts all when everything matches", () => {
38+
expect(count([1, 2, 3], () => true)).toBe(3);
39+
});
40+
});
41+
42+
describe("uniq", () => {
43+
test("removes duplicates", () => {
44+
expect(uniq([1, 2, 2, 3, 3, 3])).toEqual([1, 2, 3]);
45+
});
46+
47+
test("preserves order of first occurrence", () => {
48+
expect(uniq([3, 1, 2, 1, 3])).toEqual([3, 1, 2]);
49+
});
50+
51+
test("handles empty array", () => {
52+
expect(uniq([])).toEqual([]);
53+
});
54+
55+
test("works with strings", () => {
56+
expect(uniq(["a", "b", "a"])).toEqual(["a", "b"]);
57+
});
58+
});

src/utils/__tests__/set.test.ts

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import { describe, expect, test } from "bun:test";
2+
import { difference, every, intersects, union } from "../set";
3+
4+
describe("difference", () => {
5+
test("returns elements in a but not in b", () => {
6+
const result = difference(new Set([1, 2, 3]), new Set([2, 3, 4]));
7+
expect(result).toEqual(new Set([1]));
8+
});
9+
10+
test("returns empty set when a is subset of b", () => {
11+
expect(difference(new Set([1, 2]), new Set([1, 2, 3]))).toEqual(new Set());
12+
});
13+
14+
test("returns a when b is empty", () => {
15+
expect(difference(new Set([1, 2]), new Set())).toEqual(new Set([1, 2]));
16+
});
17+
});
18+
19+
describe("intersects", () => {
20+
test("returns true when sets share elements", () => {
21+
expect(intersects(new Set([1, 2]), new Set([2, 3]))).toBe(true);
22+
});
23+
24+
test("returns false when sets are disjoint", () => {
25+
expect(intersects(new Set([1, 2]), new Set([3, 4]))).toBe(false);
26+
});
27+
28+
test("returns false for empty sets", () => {
29+
expect(intersects(new Set(), new Set([1]))).toBe(false);
30+
expect(intersects(new Set([1]), new Set())).toBe(false);
31+
});
32+
});
33+
34+
describe("every", () => {
35+
test("returns true when a is subset of b", () => {
36+
expect(every(new Set([1, 2]), new Set([1, 2, 3]))).toBe(true);
37+
});
38+
39+
test("returns false when a has elements not in b", () => {
40+
expect(every(new Set([1, 4]), new Set([1, 2, 3]))).toBe(false);
41+
});
42+
43+
test("returns true for empty a", () => {
44+
expect(every(new Set(), new Set([1, 2]))).toBe(true);
45+
});
46+
});
47+
48+
describe("union", () => {
49+
test("combines both sets", () => {
50+
const result = union(new Set([1, 2]), new Set([3, 4]));
51+
expect(result).toEqual(new Set([1, 2, 3, 4]));
52+
});
53+
54+
test("deduplicates shared elements", () => {
55+
const result = union(new Set([1, 2]), new Set([2, 3]));
56+
expect(result).toEqual(new Set([1, 2, 3]));
57+
});
58+
59+
test("handles empty sets", () => {
60+
expect(union(new Set(), new Set([1]))).toEqual(new Set([1]));
61+
expect(union(new Set([1]), new Set())).toEqual(new Set([1]));
62+
});
63+
});

0 commit comments

Comments
 (0)