Files
oh-my-pi/packages/coding-agent/test/modes/components/segment-track.test.ts
T
can1357 9f79c5e353 feat(coding-agent): Color segment track segments by position on HSV hue band
Dynamically assigns segment colors by sweeping across the full HSV hue spectrum based on their track position. This ensures each segment has a distinct, predictable hue and removes the need for explicit `color` assignments.
2026-06-12 10:37:21 +02:00

92 lines
3.9 KiB
TypeScript

import { beforeAll, describe, expect, it } from "bun:test";
import {
renderSegmentTrack,
resolveSegmentPalette,
type TrackSegment,
} from "@oh-my-pi/pi-coding-agent/modes/components/segment-track";
import { initTheme, type ThemeColor, theme } from "@oh-my-pi/pi-coding-agent/modes/theme/theme";
beforeAll(async () => {
await initTheme();
});
const SEGMENTS: TrackSegment[] = [{ label: "smol" }, { label: "default" }, { label: "slow" }];
/** Pull the RGB out of a truecolor fg/bg escape, or null for 256-palette ones. */
function rgb(ansi: string): [number, number, number] | null {
const m = /3[84];2;(\d+);(\d+);(\d+)/.exec(ansi);
return m ? [Number(m[1]), Number(m[2]), Number(m[3])] : null;
}
function luma([r, g, b]: [number, number, number]): number {
return 0.299 * r + 0.587 * g + 0.114 * b;
}
describe("resolveSegmentPalette", () => {
it("returns theme colors that resolve to pairwise-distinct escapes", () => {
const palette = resolveSegmentPalette(SEGMENTS.length);
expect(palette.length).toBeGreaterThan(0);
expect(palette.length).toBeLessThanOrEqual(SEGMENTS.length);
const escapes = palette.map(color => theme.getFgAnsi(color));
expect(new Set(escapes).size).toBe(palette.length);
});
it("starts from the theme accent and never exceeds the requested count", () => {
expect(resolveSegmentPalette(1)).toEqual(["accent"]);
expect(resolveSegmentPalette(4).length).toBeLessThanOrEqual(4);
});
});
describe("renderSegmentTrack", () => {
it("colors each segment by position from the theme palette", () => {
const raw = renderSegmentTrack(SEGMENTS, 1);
expect(Bun.stripANSI(raw)).toContain("smol");
expect(Bun.stripANSI(raw)).toContain("default");
expect(Bun.stripANSI(raw)).toContain("slow");
// Each position carries the escape of its palette slot (modulo wrap).
const palette = resolveSegmentPalette(SEGMENTS.length);
for (let i = 0; i < SEGMENTS.length; i++) {
expect(raw).toContain(theme.getFgAnsi(palette[i % palette.length]));
}
});
it("fills exactly the active segment as a bold chip with a background", () => {
const raw = renderSegmentTrack(SEGMENTS, 1);
// One filled chip: a single bold run and a single background fill. The bg
// introducer is `48;2;` on truecolor terminals and `48;5;` on 256-palette
// ones (CI), so match the palette-agnostic `\x1b[48;` prefix.
expect(raw.match(/\x1b\[1m/g)?.length).toBe(1);
expect(raw.match(/\x1b\[48;/g)?.length).toBe(1);
// The active label sits inside the bold run, and the fill is its own slot color.
expect(raw).toContain("\x1b[1m default \x1b[22m");
const palette = resolveSegmentPalette(SEGMENTS.length);
const activeBg = theme.getFgAnsi(palette[1 % palette.length]).replace("\x1b[38;", "\x1b[48;");
expect(raw).toContain(activeBg);
});
it("moves the filled chip with the active index", () => {
expect(renderSegmentTrack(SEGMENTS, 0)).toContain("\x1b[1m smol \x1b[22m");
expect(renderSegmentTrack(SEGMENTS, 2)).toContain("\x1b[1m slow \x1b[22m");
// A non-active label is never wrapped in the bold chip run.
expect(renderSegmentTrack(SEGMENTS, 0)).not.toContain("\x1b[1m slow \x1b[22m");
});
});
describe("theme.getContrastFgAnsi", () => {
it("returns a high-contrast near-black/near-white over any fill", () => {
const BLACK = "\x1b[38;2;0;0;0m";
const WHITE = "\x1b[38;2;255;255;255m";
const colors: ThemeColor[] = ["warning", "accent", "success", "error", "border", "muted", "text"];
for (const color of colors) {
const fill = rgb(theme.getFgAnsi(color));
if (!fill) continue; // 256-palette terminal: falls back to `text`, not under test here
const picked = theme.getContrastFgAnsi(color);
expect(picked === BLACK || picked === WHITE).toBe(true);
const pickedRgb = rgb(picked);
expect(pickedRgb).not.toBeNull();
// Whichever it picked must read clearly against the fill.
expect(Math.abs(luma(pickedRgb as [number, number, number]) - luma(fill))).toBeGreaterThan(100);
}
});
});