diff options
| author | Craig Jennings <c@cjennings.net> | 2026-06-24 14:44:28 -0400 |
|---|---|---|
| committer | Craig Jennings <c@cjennings.net> | 2026-06-24 16:15:24 -0400 |
| commit | fa5b28ea69f3bff0941f8a097a9746b7a67fa900 (patch) | |
| tree | 71571b286b77b9168de3308f50877ad7f6fa4854 /scripts/theme-studio/test_generate.py | |
| parent | c11ad211f5d72b6ee2b48d80f25d16e3e85248eb (diff) | |
| download | dotemacs-fa5b28ea69f3bff0941f8a097a9746b7a67fa900.tar.gz dotemacs-fa5b28ea69f3bff0941f8a097a9746b7a67fa900.zip | |
feat(theme-studio): nerd-icons gallery as a hue-ordered icon grid
The nerd-icons pane is now a grid: one row per color face, the rows ordered by hue so families cluster, distinct icons (deduped within a color) drawn in their color with the icon's nerd-font name beneath. A "preview:" dropdown above the grid picks the glyph size in points, with Left/Right arrows to step it. Single-pane apps show it disabled, naming the preview. This replaces the v1 legend in the pane, whose data is still captured for round-trip.
build-nerd-icons-legend.el is now a library. A cj/nerd-icons-write-legend entry point requires nerd-icons only at write time, so the capture logic loads and unit-tests without it. It dedupes icons by name within a face, computes each face's native hue, and orders the groups by hue. Writing the test surfaced a latent bug: face-hsl used (cadr (assoc t spec)), which grabs the first keyword instead of the plist. It only worked because the real faces fall through to the face-foreground branch. I fixed it to a correct t-clause parse.
Coverage: 7 ERT capture tests (dedupe, hue order, lightness tiebreak, name sort, skip rules), 4 Python validator edges, and browser gates for the grid and the size dropdown.
Locate stays color-level: clicking a color flashes its icons, and clicking an icon flashes its color row. Icons aren't individually editable, so there's nothing per-icon to select.
Diffstat (limited to 'scripts/theme-studio/test_generate.py')
| -rw-r--r-- | scripts/theme-studio/test_generate.py | 109 |
1 files changed, 109 insertions, 0 deletions
diff --git a/scripts/theme-studio/test_generate.py b/scripts/theme-studio/test_generate.py index 58541cbae..bc0e87815 100644 --- a/scripts/theme-studio/test_generate.py +++ b/scripts/theme-studio/test_generate.py @@ -656,6 +656,115 @@ class NerdIconsLegend(unittest.TestCase): self.assertIn("nerd-icons-blue", rows) self.assertTrue(rows["nerd-icons-blue"], "nerd-icons-blue should carry a native seed") + def test_legend_loads_from_object_shaped_artifact(self): + # The committed artifact is now an object {legend, gallery}; the legend + # loader must read the "legend" key, not assume a bare array. + path = self._write(json.dumps({"legend": [ + {"key": "ext:el", "label": "init.el", "face": "nerd-icons-purple", + "category": "extension", "glyph": "x"}], "gallery": []})) + rows = generate.load_nerd_icons_legend(path) + self.assertEqual(len(rows), 1) + self.assertEqual(rows[0]["face"], "nerd-icons-purple") + + +class NerdIconsGallery(unittest.TestCase): + """The committed gallery (full colored catalog) and its loader fallback.""" + + def _write(self, content): + path = os.path.join(tempfile.mkdtemp(), "nerd-icons-legend.json") + with open(path, "w") as out: + out.write(content) + return path + + def test_committed_artifact_has_valid_groups(self): + groups = generate.load_nerd_icons_gallery() + self.assertIsNotNone(groups, "committed gallery should load") + self.assertTrue(groups) + for g in groups: + self.assertTrue(g["face"].startswith("nerd-icons-")) + self.assertIsInstance(g["hue"], (int, float)) + self.assertTrue(g["glyphs"]) + for e in g["glyphs"]: + for field in generate.NERD_ICONS_GALLERY_GLYPH_FIELDS: + self.assertIsInstance(e.get(field), str) + self.assertTrue(e[field]) + + def test_groups_are_ordered_by_hue(self): + groups = generate.load_nerd_icons_gallery() + hues = [g["hue"] for g in groups] + self.assertEqual(hues, sorted(hues), "color rows cluster by hue (ascending)") + + def test_icons_are_deduplicated_within_a_group(self): + for g in generate.load_nerd_icons_gallery(): + names = [e["name"] for e in g["glyphs"]] + self.assertEqual(len(names), len(set(names)), f"{g['face']} repeats an icon name") + + def test_absent_artifact_falls_back_to_none(self): + with redirect_stdout(io.StringIO()): + self.assertIsNone(generate.load_nerd_icons_gallery("/no/such/legend.json")) + + def test_malformed_artifact_falls_back_to_none(self): + path = self._write("{not json") + with redirect_stdout(io.StringIO()): + self.assertIsNone(generate.load_nerd_icons_gallery(path)) + + def test_legacy_array_only_artifact_has_no_gallery(self): + # A v1-era bare-array file carries a legend but no gallery -> None, no crash. + path = self._write(json.dumps([{"key": "ext:el"}])) + with redirect_stdout(io.StringIO()): + self.assertIsNone(generate.load_nerd_icons_gallery(path)) + + def test_group_missing_a_field_falls_back_to_none(self): + # Missing hue and glyphs -> invalid. + path = self._write(json.dumps({"legend": [], "gallery": [{"face": "nerd-icons-blue"}]})) + with redirect_stdout(io.StringIO()) as out: + self.assertIsNone(generate.load_nerd_icons_gallery(path)) + self.assertIn("invalid", out.getvalue()) + + def test_glyph_entry_missing_a_field_falls_back_to_none(self): + path = self._write(json.dumps({"gallery": [ + {"face": "nerd-icons-blue", "hue": 212, "glyphs": [{"glyph": "x"}]}]})) + with redirect_stdout(io.StringIO()) as out: + self.assertIsNone(generate.load_nerd_icons_gallery(path)) + self.assertIn("invalid", out.getvalue()) + + def test_group_with_empty_glyphs_falls_back_to_none(self): + path = self._write(json.dumps({"gallery": [ + {"face": "nerd-icons-blue", "hue": 212, "glyphs": []}]})) + with redirect_stdout(io.StringIO()) as out: + self.assertIsNone(generate.load_nerd_icons_gallery(path)) + self.assertIn("invalid", out.getvalue()) + + def test_group_with_a_foreign_face_falls_back_to_none(self): + path = self._write(json.dumps({"gallery": [ + {"face": "rainbow-delimiters-depth-1", "hue": 212, + "glyphs": [{"glyph": "x", "name": "nf-x"}]}]})) + with redirect_stdout(io.StringIO()) as out: + self.assertIsNone(generate.load_nerd_icons_gallery(path)) + self.assertIn("invalid", out.getvalue()) + + def test_group_with_a_non_numeric_hue_falls_back_to_none(self): + path = self._write(json.dumps({"gallery": [ + {"face": "nerd-icons-blue", "hue": "212", + "glyphs": [{"glyph": "x", "name": "nf-x"}]}]})) + with redirect_stdout(io.StringIO()) as out: + self.assertIsNone(generate.load_nerd_icons_gallery(path)) + self.assertIn("invalid", out.getvalue()) + + def test_non_dict_glyph_entry_falls_back_to_none(self): + path = self._write(json.dumps({"gallery": [ + {"face": "nerd-icons-blue", "hue": 212, "glyphs": ["not-a-dict"]}]})) + with redirect_stdout(io.StringIO()) as out: + self.assertIsNone(generate.load_nerd_icons_gallery(path)) + self.assertIn("invalid", out.getvalue()) + + def test_nerd_icons_app_carries_the_gallery(self): + app = generate.APPS.get("nerd-icons") + self.assertIsNotNone(app) + self.assertTrue(app.get("gallery"), "nerd-icons app should carry the gallery groups") + faces = {g["face"] for g in app["gallery"]} + self.assertIn("nerd-icons-blue", faces) + if __name__ == "__main__": unittest.main() |
