From e443a8fa5191dd9c5b6830d380b6d0226a59b41d Mon Sep 17 00:00:00 2001 From: claude-code-best Date: Wed, 1 Apr 2026 02:08:38 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=90=AD=E5=BB=BA=E5=8D=95=E5=85=83?= =?UTF-8?q?=E6=B5=8B=E8=AF=95=E5=9F=BA=E7=A1=80=E8=AE=BE=E6=96=BD=20?= =?UTF-8?q?=E2=80=94=20Bun=20test=20runner=20+=20=E7=A4=BA=E4=BE=8B?= =?UTF-8?q?=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 添加 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 --- TODO.md | 2 +- bunfig.toml | 3 + package.json | 3 +- .../src/__tests__/color-diff.test.ts | 102 ++++++++++++++++++ src/utils/__tests__/array.test.ts | 58 ++++++++++ src/utils/__tests__/set.test.ts | 63 +++++++++++ 6 files changed, 229 insertions(+), 2 deletions(-) create mode 100644 bunfig.toml create mode 100644 packages/color-diff-napi/src/__tests__/color-diff.test.ts create mode 100644 src/utils/__tests__/array.test.ts create mode 100644 src/utils/__tests__/set.test.ts diff --git a/TODO.md b/TODO.md index 41363c0..bc76997 100644 --- a/TODO.md +++ b/TODO.md @@ -21,5 +21,5 @@ - [ ] 冗余代码检查 - [x] git hook 的配置 - [ ] 代码健康度检查 -- [ ] 单元测试基础设施搭建 (test runner 配置) +- [x] 单元测试基础设施搭建 (test runner 配置) - [ ] CI/CD 流水线 (GitHub Actions) diff --git a/bunfig.toml b/bunfig.toml new file mode 100644 index 0000000..22f2298 --- /dev/null +++ b/bunfig.toml @@ -0,0 +1,3 @@ +[test] +root = "." +timeout = 10000 diff --git a/package.json b/package.json index 1c01bda..a8da522 100644 --- a/package.json +++ b/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", diff --git a/packages/color-diff-napi/src/__tests__/color-diff.test.ts b/packages/color-diff-napi/src/__tests__/color-diff.test.ts new file mode 100644 index 0000000..0e38a3c --- /dev/null +++ b/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); + }); +}); diff --git a/src/utils/__tests__/array.test.ts b/src/utils/__tests__/array.test.ts new file mode 100644 index 0000000..16f45bd --- /dev/null +++ b/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"]); + }); +}); diff --git a/src/utils/__tests__/set.test.ts b/src/utils/__tests__/set.test.ts new file mode 100644 index 0000000..fd176e0 --- /dev/null +++ b/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])); + }); +});