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
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
|
"""Tests for docs/prototypes/gen_tokens.py.
gen_tokens.py is the single-source token generator for the panel widget
gallery. It reads docs/prototypes/tokens.json (the neutral source of truth
for the design tokens) and emits three target representations from it:
- web CSS custom properties (:root { --gold:#e2a038; ... })
- waybar GTK CSS (@define-color gold #e2a038; ...)
- Emacs elisp (an alist for the future svg.el renderer)
The three differ on purpose: CSS uses hyphenated --vars and stores glow
colors as bare "r,g,b" triples (so rgba(var(--glow-hi),.5) works); GTK CSS
has no custom properties, so it uses @define-color with underscore names
and resolves glows to their source hex (GTK uses alpha(@color,a)); elisp
uses a hyphenated-symbol alist of hex strings. One source, three emitters
is the whole reason the generator earns its keep across the three targets.
These tests import the REAL gen_tokens module from docs/prototypes/ (not a
copy) and exercise its functions directly against fixture token dicts, plus
one integration test that runs main() against temp copies.
Run from repo root:
make test-unit
(or python3 -m unittest tests.gallery-tokens.test_gen_tokens)
"""
import importlib.util
import json
import os
import tempfile
import unittest
REPO_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", ".."))
GEN = os.path.join(REPO_ROOT, "docs", "prototypes", "gen_tokens.py")
TOKENS_JSON = os.path.join(REPO_ROOT, "docs", "prototypes", "tokens.json")
def _load_module():
"""Import the real gen_tokens.py by path (no importable package name)."""
spec = importlib.util.spec_from_file_location("gen_tokens", GEN)
mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mod)
return mod
gt = _load_module()
# A small deterministic fixture — not the real tokens.json, so these tests
# don't drift when the palette is retuned.
FIXTURE = {
"palette": {"ground": "#151311", "wash": "#2c2f32", "slate-hi": "#54677d"},
"amber": {"gold": "#e2a038", "gold-hi": "#ffbe54", "amber-grad-top": "#f2c76a"},
"glow": {"glow-hi": "gold-hi", "glow-lo": "gold"},
"font": {"mono": '"Berkeley Mono",monospace'},
"timing": {"pulse-rate": "1s"},
}
class HexToTriple(unittest.TestCase):
def test_normal_values(self):
self.assertEqual(gt.hex_to_triple("#ffbe54"), "255,190,84")
self.assertEqual(gt.hex_to_triple("#e2a038"), "226,160,56")
def test_boundary_black_and_white(self):
self.assertEqual(gt.hex_to_triple("#000000"), "0,0,0")
self.assertEqual(gt.hex_to_triple("#ffffff"), "255,255,255")
def test_boundary_three_digit_shorthand(self):
self.assertEqual(gt.hex_to_triple("#fff"), "255,255,255")
self.assertEqual(gt.hex_to_triple("#0f0"), "0,255,0")
def test_boundary_no_leading_hash(self):
self.assertEqual(gt.hex_to_triple("e2a038"), "226,160,56")
def test_error_bad_chars(self):
with self.assertRaises(ValueError):
gt.hex_to_triple("#gggggg")
def test_error_bad_length(self):
with self.assertRaises(ValueError):
gt.hex_to_triple("#12")
class ResolveColor(unittest.TestCase):
def test_finds_in_amber(self):
self.assertEqual(gt.resolve_color(FIXTURE, "gold"), "#e2a038")
def test_finds_in_palette(self):
self.assertEqual(gt.resolve_color(FIXTURE, "wash"), "#2c2f32")
def test_missing_raises(self):
with self.assertRaises(KeyError):
gt.resolve_color(FIXTURE, "nonexistent")
class EmitWebCss(unittest.TestCase):
def setUp(self):
self.css = gt.emit_web_css(FIXTURE)
def test_solid_colors_are_hyphenated_vars(self):
self.assertIn("--gold:#e2a038;", self.css)
self.assertIn("--slate-hi:#54677d;", self.css)
self.assertIn("--amber-grad-top:#f2c76a;", self.css)
def test_glow_stored_as_rgb_triple(self):
# so rgba(var(--glow-hi),.5) resolves; derived from the source hex
self.assertIn("--glow-hi:255,190,84;", self.css)
self.assertIn("--glow-lo:226,160,56;", self.css)
def test_font_and_timing(self):
self.assertIn("--pulse-rate:1s;", self.css)
self.assertIn('--mono:"Berkeley Mono",monospace;', self.css)
class EmitWaybarGtk(unittest.TestCase):
def setUp(self):
self.gtk = gt.emit_waybar_gtk(FIXTURE)
def test_uses_define_color_not_custom_props(self):
self.assertIn("@define-color gold #e2a038;", self.gtk)
self.assertNotIn("--gold", self.gtk)
self.assertNotIn(":root", self.gtk)
def test_glow_resolved_to_source_hex(self):
# GTK has no bare triples; it uses alpha(@color,a), so glow -> hex
self.assertIn("@define-color glow_hi #ffbe54;", self.gtk)
self.assertIn("@define-color glow_lo #e2a038;", self.gtk)
def test_names_use_underscores_not_hyphens(self):
self.assertIn("@define-color slate_hi #54677d;", self.gtk)
self.assertNotIn("slate-hi", self.gtk)
self.assertNotIn("amber-grad-top", self.gtk)
class EmitElisp(unittest.TestCase):
def setUp(self):
self.el = gt.emit_elisp(FIXTURE)
def test_alist_of_hex_with_hyphenated_symbols(self):
self.assertIn('(gold . "#e2a038")', self.el)
self.assertIn('(slate-hi . "#54677d")', self.el)
def test_glow_is_hex_for_svg(self):
# svg.el / librsvg wants a real color, not a bare triple
self.assertIn('(glow-hi . "#ffbe54")', self.el)
def test_timing_included(self):
self.assertIn('(pulse-rate . "1s")', self.el)
class ReplaceBetweenMarkers(unittest.TestCase):
START = "/* @tokens:start */"
END = "/* @tokens:end */"
def _wrap(self, inner):
return f"a\n{self.START}\n{inner}\n{self.END}\nb\n"
def test_replaces_inner_block(self):
src = self._wrap("OLD")
out = gt.replace_between_markers(src, self.START, self.END, "NEW")
self.assertIn("NEW", out)
self.assertNotIn("OLD", out)
# markers and surrounding text survive
self.assertIn(self.START, out)
self.assertIn(self.END, out)
self.assertTrue(out.startswith("a\n"))
self.assertTrue(out.rstrip().endswith("b"))
def test_idempotent(self):
src = self._wrap("OLD")
once = gt.replace_between_markers(src, self.START, self.END, "NEW")
twice = gt.replace_between_markers(once, self.START, self.END, "NEW")
self.assertEqual(once, twice)
def test_missing_marker_raises(self):
with self.assertRaises(ValueError):
gt.replace_between_markers("no markers here", self.START, self.END, "NEW")
def test_start_marker_on_final_line_without_newline(self):
# Defensive branch: no newline after the start marker. The guard must
# not let text[:si-of-newline+1] collapse to "" and wipe the prefix.
src = f"keep\n{self.START} {self.END}"
out = gt.replace_between_markers(src, self.START, self.END, "X")
self.assertTrue(out.startswith("keep\n"))
self.assertIn(self.END, out)
self.assertIn("X", out)
class RealTokensJson(unittest.TestCase):
"""The committed tokens.json must stay well-formed and complete."""
def setUp(self):
with open(TOKENS_JSON) as f:
self.tokens = json.load(f)
def test_has_required_sections(self):
for section in ("palette", "amber", "glow", "font", "timing"):
self.assertIn(section, self.tokens)
def test_amber_defines_gold_and_glow_sources_resolve(self):
self.assertIn("gold", self.tokens["amber"])
self.assertIn("gold-hi", self.tokens["amber"])
for _name, source in self.tokens["glow"].items():
# every glow source must resolve to a real color
gt.resolve_color(self.tokens, source)
def test_pulse_rate_present(self):
self.assertIn("pulse-rate", self.tokens["timing"])
class DefaultElispFilename(unittest.TestCase):
"""The default elisp target must be gallery-tokens.el so that the file's
(provide 'gallery-tokens) matches its name and `require` can resolve it
from a load-path. A tokens.el/gallery-tokens feature mismatch is a trap."""
def test_default_elisp_path_matches_provided_feature(self):
self.assertEqual(
os.path.basename(gt.DEFAULT_ELISP_NAME), "gallery-tokens.el")
class MainIntegration(unittest.TestCase):
"""main() rewrites the html :root and writes the waybar + elisp files."""
def setUp(self):
self.tmp = tempfile.mkdtemp(prefix="gen-tokens-test-")
self.html = os.path.join(self.tmp, "gallery.html")
self.tokens_path = os.path.join(self.tmp, "tokens.json")
self.waybar = os.path.join(self.tmp, "tokens-waybar.css")
self.elisp = os.path.join(self.tmp, "gallery-tokens.el")
with open(self.tokens_path, "w") as f:
json.dump(FIXTURE, f)
with open(self.html, "w") as f:
f.write(
"<style>\n:root{\n"
"/* @tokens:start */\n"
"STALE\n"
"/* @tokens:end */\n"
"}\n</style>\n"
)
def tearDown(self):
import shutil
shutil.rmtree(self.tmp, ignore_errors=True)
def test_main_writes_all_three_targets(self):
gt.main(
tokens_path=self.tokens_path,
html_path=self.html,
waybar_path=self.waybar,
elisp_path=self.elisp,
)
with open(self.html) as f:
html = f.read()
self.assertIn("--gold:#e2a038;", html)
self.assertNotIn("STALE", html)
self.assertTrue(os.path.exists(self.waybar))
self.assertTrue(os.path.exists(self.elisp))
with open(self.waybar) as f:
self.assertIn("@define-color gold #e2a038;", f.read())
with open(self.elisp) as f:
self.assertIn('(gold . "#e2a038")', f.read())
def test_main_is_idempotent(self):
gt.main(tokens_path=self.tokens_path, html_path=self.html,
waybar_path=self.waybar, elisp_path=self.elisp)
with open(self.html) as f:
first = f.read()
gt.main(tokens_path=self.tokens_path, html_path=self.html,
waybar_path=self.waybar, elisp_path=self.elisp)
with open(self.html) as f:
second = f.read()
self.assertEqual(first, second)
if __name__ == "__main__":
unittest.main()
|