aboutsummaryrefslogtreecommitdiff
path: root/tests
diff options
context:
space:
mode:
Diffstat (limited to 'tests')
-rw-r--r--tests/gallery-tokens/test_gen_tokens.py268
1 files changed, 268 insertions, 0 deletions
diff --git a/tests/gallery-tokens/test_gen_tokens.py b/tests/gallery-tokens/test_gen_tokens.py
new file mode 100644
index 0000000..414194e
--- /dev/null
+++ b/tests/gallery-tokens/test_gen_tokens.py
@@ -0,0 +1,268 @@
+"""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 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, "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()