Skip to content

Commit 9ddf7c6

Browse files
authored
test: util — Color, signal, and defer coverage (#379)
Add unit tests for three untested utility modules that power TUI rendering and async resource management. Color.hexToRgb NaN propagation, signal() race conditions, and defer() TC39 dispose patterns are all verified. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> https://claude.ai/code/session_0154uqXBVVoY4G5HcLzFfGRY
1 parent d10303c commit 9ddf7c6

File tree

3 files changed

+151
-0
lines changed

3 files changed

+151
-0
lines changed
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import { describe, test, expect } from "bun:test"
2+
import { Color } from "../../src/util/color"
3+
4+
describe("Color.isValidHex", () => {
5+
test("accepts valid 6-digit hex codes", () => {
6+
expect(Color.isValidHex("#ff5733")).toBe(true)
7+
expect(Color.isValidHex("#000000")).toBe(true)
8+
expect(Color.isValidHex("#FFFFFF")).toBe(true)
9+
expect(Color.isValidHex("#aAbBcC")).toBe(true)
10+
})
11+
12+
test("rejects 3-digit shorthand hex", () => {
13+
// Users coming from CSS might try #FFF — this must be rejected
14+
// because hexToRgb assumes 6-digit format
15+
expect(Color.isValidHex("#FFF")).toBe(false)
16+
expect(Color.isValidHex("#abc")).toBe(false)
17+
})
18+
19+
test("rejects missing hash prefix", () => {
20+
expect(Color.isValidHex("ff5733")).toBe(false)
21+
})
22+
23+
test("rejects empty, undefined, and null-ish values", () => {
24+
expect(Color.isValidHex("")).toBe(false)
25+
expect(Color.isValidHex(undefined)).toBe(false)
26+
})
27+
28+
test("rejects hex with invalid characters", () => {
29+
expect(Color.isValidHex("#gggggg")).toBe(false)
30+
expect(Color.isValidHex("#12345z")).toBe(false)
31+
})
32+
33+
test("rejects hex with wrong length", () => {
34+
expect(Color.isValidHex("#1234567")).toBe(false)
35+
expect(Color.isValidHex("#12345")).toBe(false)
36+
})
37+
})
38+
39+
describe("Color.hexToRgb", () => {
40+
test("converts standard hex to correct RGB", () => {
41+
expect(Color.hexToRgb("#ff0000")).toEqual({ r: 255, g: 0, b: 0 })
42+
expect(Color.hexToRgb("#00ff00")).toEqual({ r: 0, g: 255, b: 0 })
43+
expect(Color.hexToRgb("#0000ff")).toEqual({ r: 0, g: 0, b: 255 })
44+
})
45+
46+
test("handles boundary values", () => {
47+
expect(Color.hexToRgb("#000000")).toEqual({ r: 0, g: 0, b: 0 })
48+
expect(Color.hexToRgb("#ffffff")).toEqual({ r: 255, g: 255, b: 255 })
49+
})
50+
51+
test("handles mixed case", () => {
52+
expect(Color.hexToRgb("#AaBbCc")).toEqual({ r: 170, g: 187, b: 204 })
53+
})
54+
})
55+
56+
describe("Color.hexToAnsiBold", () => {
57+
test("produces correct ANSI escape for valid hex", () => {
58+
const result = Color.hexToAnsiBold("#ff0000")
59+
expect(result).toBe("\x1b[38;2;255;0;0m\x1b[1m")
60+
})
61+
62+
test("returns undefined for invalid hex, preventing NaN in ANSI sequences", () => {
63+
// Key safety test: without isValidHex guard, hexToRgb("#bad") would
64+
// produce NaN values in the escape sequence, corrupting terminal output
65+
expect(Color.hexToAnsiBold("#bad")).toBeUndefined()
66+
expect(Color.hexToAnsiBold("")).toBeUndefined()
67+
expect(Color.hexToAnsiBold(undefined)).toBeUndefined()
68+
})
69+
})
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { describe, test, expect } from "bun:test"
2+
import { defer } from "../../src/util/defer"
3+
4+
describe("defer", () => {
5+
test("Symbol.dispose calls the cleanup function", () => {
6+
let called = false
7+
const d = defer(() => {
8+
called = true
9+
})
10+
d[Symbol.dispose]()
11+
expect(called).toBe(true)
12+
})
13+
14+
test("Symbol.asyncDispose calls and awaits the cleanup function", async () => {
15+
let called = false
16+
const d = defer(async () => {
17+
called = true
18+
})
19+
await d[Symbol.asyncDispose]()
20+
expect(called).toBe(true)
21+
})
22+
23+
test("works with using statement for sync cleanup", () => {
24+
let cleaned = false
25+
{
26+
using _ = defer(() => {
27+
cleaned = true
28+
})
29+
}
30+
expect(cleaned).toBe(true)
31+
})
32+
33+
test("works with await using statement for async cleanup", async () => {
34+
let cleaned = false
35+
{
36+
await using _ = defer(async () => {
37+
cleaned = true
38+
})
39+
}
40+
expect(cleaned).toBe(true)
41+
})
42+
})
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { describe, test, expect } from "bun:test"
2+
import { signal } from "../../src/util/signal"
3+
4+
describe("signal", () => {
5+
test("wait() resolves after trigger()", async () => {
6+
const s = signal()
7+
let resolved = false
8+
const waiting = s.wait().then(() => {
9+
resolved = true
10+
})
11+
expect(resolved).toBe(false)
12+
s.trigger()
13+
await waiting
14+
expect(resolved).toBe(true)
15+
})
16+
17+
test("trigger() before wait() still resolves immediately", async () => {
18+
// Race condition guard: trigger fires before anyone awaits
19+
const s = signal()
20+
s.trigger()
21+
let resolved = false
22+
await s.wait().then(() => {
23+
resolved = true
24+
})
25+
expect(resolved).toBe(true)
26+
})
27+
28+
test("multiple wait() calls on same signal all resolve", async () => {
29+
const s = signal()
30+
let count = 0
31+
const waiters = [
32+
s.wait().then(() => count++),
33+
s.wait().then(() => count++),
34+
s.wait().then(() => count++),
35+
]
36+
s.trigger()
37+
await Promise.all(waiters)
38+
expect(count).toBe(3)
39+
})
40+
})

0 commit comments

Comments
 (0)