|
1 | 1 | import { test, expect, describe } from "bun:test" |
2 | | -import { RGBA, hexToRgb, rgbToHex, hsvToRgb, parseColor } from "./RGBA.js" |
| 2 | +import { |
| 3 | + COLOR_TAG_DEFAULT, |
| 4 | + COLOR_TAG_RGB, |
| 5 | + RGBA, |
| 6 | + decodeColorTag, |
| 7 | + hexToRgb, |
| 8 | + hsvToRgb, |
| 9 | + normalizeColorValue, |
| 10 | + parseColor, |
| 11 | + rgbToHex, |
| 12 | +} from "./RGBA.js" |
3 | 13 |
|
4 | 14 | describe("RGBA class", () => { |
5 | 15 | describe("constructor", () => { |
6 | | - test("creates RGBA with Float32Array buffer", () => { |
7 | | - const buffer = new Float32Array([0.5, 0.6, 0.7, 0.8]) |
| 16 | + test("uses the provided 5-float buffer", () => { |
| 17 | + const buffer = new Float32Array([0.5, 0.6, 0.7, 0.8, COLOR_TAG_RGB]) |
8 | 18 | const rgba = new RGBA(buffer) |
9 | 19 | expect(rgba.buffer).toBe(buffer) |
| 20 | + expect(rgba.tag).toBe(COLOR_TAG_RGB) |
10 | 21 | }) |
11 | 22 |
|
12 | | - test("buffer is mutable reference", () => { |
| 23 | + test("upgrades legacy 4-float buffers to explicit RGB tag", () => { |
13 | 24 | const buffer = new Float32Array([0.5, 0.6, 0.7, 0.8]) |
14 | 25 | const rgba = new RGBA(buffer) |
| 26 | + |
| 27 | + expect(rgba.buffer).not.toBe(buffer) |
| 28 | + expect(rgba.buffer).toHaveLength(5) |
| 29 | + expect(rgba.r).toBeCloseTo(0.5, 5) |
| 30 | + expect(rgba.g).toBeCloseTo(0.6, 5) |
| 31 | + expect(rgba.b).toBeCloseTo(0.7, 5) |
| 32 | + expect(rgba.a).toBeCloseTo(0.8, 5) |
| 33 | + expect(rgba.tag).toBe(COLOR_TAG_RGB) |
| 34 | + }) |
| 35 | + |
| 36 | + test("buffer is mutable reference when already 5-float", () => { |
| 37 | + const buffer = new Float32Array([0.5, 0.6, 0.7, 0.8, COLOR_TAG_RGB]) |
| 38 | + const rgba = new RGBA(buffer) |
15 | 39 | buffer[0] = 0.9 |
16 | 40 | expect(rgba.r).toBeCloseTo(0.9, 5) |
17 | 41 | }) |
18 | 42 | }) |
19 | 43 |
|
20 | 44 | describe("fromArray", () => { |
21 | | - test("creates RGBA from Float32Array", () => { |
| 45 | + test("creates RGBA from legacy 4-float arrays", () => { |
22 | 46 | const array = new Float32Array([0.1, 0.2, 0.3, 0.4]) |
23 | 47 | const rgba = RGBA.fromArray(array) |
24 | 48 | expect(rgba.r).toBeCloseTo(0.1, 5) |
25 | 49 | expect(rgba.g).toBeCloseTo(0.2, 5) |
26 | 50 | expect(rgba.b).toBeCloseTo(0.3, 5) |
27 | 51 | expect(rgba.a).toBeCloseTo(0.4, 5) |
| 52 | + expect(rgba.tag).toBe(COLOR_TAG_RGB) |
28 | 53 | }) |
29 | 54 |
|
30 | | - test("uses same buffer reference", () => { |
31 | | - const array = new Float32Array([0.1, 0.2, 0.3, 0.4]) |
| 55 | + test("uses same buffer reference when already 5-float", () => { |
| 56 | + const array = new Float32Array([0.1, 0.2, 0.3, 0.4, COLOR_TAG_DEFAULT]) |
32 | 57 | const rgba = RGBA.fromArray(array) |
33 | 58 | expect(rgba.buffer).toBe(array) |
| 59 | + expect(rgba.tag).toBe(COLOR_TAG_DEFAULT) |
34 | 60 | }) |
35 | 61 | }) |
36 | 62 |
|
@@ -120,6 +146,69 @@ describe("RGBA class", () => { |
120 | 146 | }) |
121 | 147 | }) |
122 | 148 |
|
| 149 | + describe("clone", () => { |
| 150 | + test("creates a detached copy", () => { |
| 151 | + const original = RGBA.fromValues(0.1, 0.2, 0.3, 0.4) |
| 152 | + const cloned = RGBA.clone(original) |
| 153 | + expect(cloned).not.toBe(original) |
| 154 | + expect(cloned.buffer).not.toBe(original.buffer) |
| 155 | + expect(cloned.toInts()).toEqual(original.toInts()) |
| 156 | + cloned.r = 0.9 |
| 157 | + expect(original.r).toBeCloseTo(0.1, 5) |
| 158 | + }) |
| 159 | + }) |
| 160 | + |
| 161 | + describe("intent helpers", () => { |
| 162 | + test("fromIndex uses ANSI256 fallback snapshots", () => { |
| 163 | + expect(RGBA.fromIndex(9).toInts()).toEqual([255, 0, 0, 255]) |
| 164 | + expect(RGBA.fromIndex(21).toInts()).toEqual([0, 0, 255, 255]) |
| 165 | + expect(RGBA.fromIndex(232).toInts()).toEqual([8, 8, 8, 255]) |
| 166 | + expect(RGBA.fromIndex(255).toInts()).toEqual([238, 238, 238, 255]) |
| 167 | + }) |
| 168 | + |
| 169 | + test("stores explicit tags in the fifth float", () => { |
| 170 | + const rgb = RGBA.fromHex("#112233") |
| 171 | + const indexed = RGBA.fromIndex(6) |
| 172 | + const defaultFg = RGBA.defaultForeground() |
| 173 | + |
| 174 | + expect(rgb.buffer).toHaveLength(5) |
| 175 | + expect(rgb.buffer[4]).toBe(COLOR_TAG_RGB) |
| 176 | + expect(indexed.buffer[4]).toBe(6) |
| 177 | + expect(defaultFg.buffer[4]).toBe(COLOR_TAG_DEFAULT) |
| 178 | + }) |
| 179 | + |
| 180 | + test("does not mutate caller-owned snapshots when constructing tagged colors", () => { |
| 181 | + const indexedSnapshot = RGBA.fromHex("#112233") |
| 182 | + const defaultSnapshot = RGBA.fromHex("#abcdef") |
| 183 | + |
| 184 | + const indexed = RGBA.fromIndex(6, indexedSnapshot) |
| 185 | + const defaultFg = RGBA.defaultForeground(defaultSnapshot) |
| 186 | + |
| 187 | + expect(indexed).not.toBe(indexedSnapshot) |
| 188 | + expect(defaultFg).not.toBe(defaultSnapshot) |
| 189 | + expect(RGBA.getIntentTag(indexedSnapshot)).toBe(COLOR_TAG_RGB) |
| 190 | + expect(RGBA.getIntentTag(defaultSnapshot)).toBe(COLOR_TAG_RGB) |
| 191 | + expect(RGBA.getIntentTag(indexed)).toBe(6) |
| 192 | + expect(RGBA.getIntentTag(defaultFg)).toBe(COLOR_TAG_DEFAULT) |
| 193 | + expect(indexed.toInts()).toEqual(indexedSnapshot.toInts()) |
| 194 | + expect(defaultFg.toInts()).toEqual(defaultSnapshot.toInts()) |
| 195 | + }) |
| 196 | + |
| 197 | + test("normalizeColorValue and decodeColorTag preserve color intent", () => { |
| 198 | + expect(normalizeColorValue(null)).toBeNull() |
| 199 | + expect( |
| 200 | + [RGBA.fromHex("#123456"), RGBA.fromIndex(12), RGBA.defaultBackground()].map((color) => [ |
| 201 | + normalizeColorValue(color)?.tag, |
| 202 | + decodeColorTag(color.tag), |
| 203 | + ]), |
| 204 | + ).toEqual([ |
| 205 | + [COLOR_TAG_RGB, { kind: "rgb" }], |
| 206 | + [12, { kind: "indexed", index: 12 }], |
| 207 | + [COLOR_TAG_DEFAULT, { kind: "default" }], |
| 208 | + ]) |
| 209 | + }) |
| 210 | + }) |
| 211 | + |
123 | 212 | describe("fromHex", () => { |
124 | 213 | test("creates RGBA from hex string", () => { |
125 | 214 | const rgba = RGBA.fromHex("#FF8040") |
@@ -274,6 +363,14 @@ describe("RGBA class", () => { |
274 | 363 | }) |
275 | 364 | }) |
276 | 365 |
|
| 366 | + describe("equals", () => { |
| 367 | + test("compares both rgba values and tags", () => { |
| 368 | + const rgb = RGBA.fromHex("#112233") |
| 369 | + expect(rgb.equals(RGBA.fromIndex(6, rgb))).toBe(false) |
| 370 | + expect(RGBA.defaultForeground("#aabbcc").equals(RGBA.defaultForeground("#aabbcc"))).toBe(true) |
| 371 | + }) |
| 372 | + }) |
| 373 | + |
277 | 374 | describe("toString", () => { |
278 | 375 | test("formats as rgba string with 2 decimal places", () => { |
279 | 376 | const rgba = RGBA.fromValues(0.5, 0.6, 0.7, 0.8) |
|
0 commit comments