File size: 4,284 Bytes
4c76b0d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
import { writeFileSync } from "node:fs";
import { join } from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import { useAutoCleanupTempDirTracker } from "../../../../../test/helpers/temp-dir.js";
import darkTheme from "./dark.json" with { type: "json" };
import { loadThemeFromPath } from "./theme.js";

const tempDirs = useAutoCleanupTempDirTracker(afterEach);

function loadColor(color: string | number, mode: "256color" | "truecolor") {
  const themePath = join(tempDirs.make("openclaw-theme-"), "theme.json");
  writeFileSync(
    themePath,
    JSON.stringify({
      ...darkTheme,
      colors: { ...darkTheme.colors, accent: color, selectedBg: color },
    }),
  );
  return loadThemeFromPath(themePath, mode);
}

describe("loadThemeFromPath", () => {
  it.each([
    ["#0000ff", 21],
    ["#2f00ff", 21],
    ["#3000ff", 57],
    ["#7200ff", 57],
    ["#7300ff", 57],
    ["#7400ff", 93],
    ["#9a00ff", 93],
    ["#9b00ff", 93],
    ["#9c00ff", 129],
    ["#c200ff", 129],
    ["#c300ff", 129],
    ["#c400ff", 165],
    ["#ea00ff", 165],
    ["#eb00ff", 165],
    ["#ec00ff", 201],
    ["#ff00ff", 201],
    ["#ff7200", 202],
    ["#ff7300", 202],
    ["#ff7400", 208],
    ["#00ff72", 47],
    ["#00ff73", 47],
    ["#00ff74", 48],
    ["#5f87af", 67],
    ["#000000", 16],
    ["#040404", 16],
    ["#050505", 232],
    ["#080808", 232],
    ["#0c0c0c", 232],
    ["#0d0d0d", 232],
    ["#0e0e0e", 233],
    ["#707070", 242],
    ["#717171", 242],
    ["#727272", 243],
    ["#e8e8e8", 254],
    ["#e9e9e9", 254],
    ["#eaeaea", 255],
    ["#eeeeee", 255],
    ["#ffffff", 231],
    ["#606060", 59],
    ["#616161", 241],
    ["#0c0d0e", 232],
    ["#0d0d16", 233],
    ["#0d0d17", 16],
  ])(
    "renders %s as palette index %i with separate foreground/background resets",
    (color, index) => {
      const theme = loadColor(color, "256color");
      expect(theme.fg("accent", "text")).toBe(`\x1b[38;5;${index}mtext\x1b[39m`);
      expect(theme.bg("selectedBg", "text")).toBe(`\x1b[48;5;${index}mtext\x1b[49m`);
    },
  );

  it("preserves RGB channels in truecolor output", () => {
    const theme = loadColor("#5f87af", "truecolor");
    expect(theme.getFgAnsi("accent")).toBe("\x1b[38;2;95;135;175m");
    expect(theme.getBgAnsi("selectedBg")).toBe("\x1b[48;2;95;135;175m");
    expect(theme.fg("accent", "text")).toBe("\x1b[38;2;95;135;175mtext\x1b[39m");
    expect(theme.bg("selectedBg", "text")).toBe("\x1b[48;2;95;135;175mtext\x1b[49m");
  });

  it.each(["256color", "truecolor"] as const)(
    "preserves numeric and reset colors in %s",
    (mode) => {
      for (const index of [0, 123, 255]) {
        const theme = loadColor(index, mode);
        expect(theme.getFgAnsi("accent")).toBe(`\x1b[38;5;${index}m`);
        expect(theme.getBgAnsi("selectedBg")).toBe(`\x1b[48;5;${index}m`);
        expect(theme.fg("accent", "text")).toBe(`\x1b[38;5;${index}mtext\x1b[39m`);
        expect(theme.bg("selectedBg", "text")).toBe(`\x1b[48;5;${index}mtext\x1b[49m`);
      }
      const reset = loadColor("", mode);
      expect(reset.getFgAnsi("accent")).toBe("\x1b[39m");
      expect(reset.getBgAnsi("selectedBg")).toBe("\x1b[49m");
      expect(reset.fg("accent", "text")).toBe("\x1b[39mtext\x1b[39m");
      expect(reset.bg("selectedBg", "text")).toBe("\x1b[49mtext\x1b[49m");
    },
  );

  it.each([
    ["fg", /^Unknown theme color: missing$/],
    ["getFgAnsi", /^Unknown theme color: missing$/],
    ["bg", /^Unknown theme background color: missing$/],
    ["getBgAnsi", /^Unknown theme background color: missing$/],
  ] as const)("rejects unknown colors through %s", (method, message) => {
    const theme = loadColor(123, "256color");
    expect(() => Reflect.apply(theme[method], theme, ["missing", "text"])).toThrow(message);
  });

  it("keeps formatting independent of supplied raw ANSI getter overrides", () => {
    const theme = loadColor(123, "256color");
    theme.getFgAnsi = () => "\x1b[31m";
    theme.getBgAnsi = () => "\x1b[41m";
    expect(theme.getFgAnsi("accent")).toBe("\x1b[31m");
    expect(theme.getBgAnsi("selectedBg")).toBe("\x1b[41m");
    expect(theme.fg("accent", "text")).toBe("\x1b[38;5;123mtext\x1b[39m");
    expect(theme.bg("selectedBg", "text")).toBe("\x1b[48;5;123mtext\x1b[49m");
  });
});