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
|
;;; test-gallery-widget.el --- ERT tests for gallery-widget.el -*- lexical-binding: t; -*-
;;; Commentary:
;; Tests for the svg.el proof-of-concept renderer in docs/prototypes/.
;; gallery-widget.el reads the generated gallery-tokens.el (same source of truth as
;; the web gallery and the waybar CSS) and renders gallery widgets as SVG.
;; These tests exercise the REAL module (loaded by path, not a copy):
;; token resolution, needle-angle math (normal/boundary/error), and the
;; rendered SVG document's structure.
;;
;; Run from repo root:
;; make test-elisp
;; (or emacs --batch -l ert -l tests/gallery-widgets/test-gallery-widget.el \
;; -f ert-run-tests-batch-and-exit)
;;; Code:
(require 'ert)
(require 'cl-lib)
(defvar test-gallery-widget--root
(expand-file-name "../.." (file-name-directory (or load-file-name buffer-file-name)))
"Repo root, derived from this test file's location.")
(load (expand-file-name "docs/prototypes/gallery-widget.el" test-gallery-widget--root))
;;; --- token access ---
(ert-deftest gallery-widget-token-resolves-hex ()
"Known tokens resolve to hex color strings from the generated alist."
(should (string-match-p "\\`#[0-9a-f]\\{6\\}\\'" (gallery-widget-token 'gold)))
(should (string-match-p "\\`#[0-9a-f]\\{6\\}\\'" (gallery-widget-token 'glow-hi)))
(should (string-match-p "\\`#[0-9a-f]\\{6\\}\\'" (gallery-widget-token 'wash))))
(ert-deftest gallery-widget-token-missing-errors ()
"An unknown token name signals an error rather than returning nil."
(should-error (gallery-widget-token 'no-such-token)))
;;; --- needle angle math ---
(ert-deftest gallery-widget-needle-angle-normal ()
"0..100 maps linearly onto -60..+60 degrees."
(should (= (gallery-widget--needle-angle 0) -60.0))
(should (= (gallery-widget--needle-angle 50) 0.0))
(should (= (gallery-widget--needle-angle 100) 60.0)))
(ert-deftest gallery-widget-needle-angle-boundary-clamps ()
"Out-of-range values clamp to the dial's ends instead of overswinging."
(should (= (gallery-widget--needle-angle -5) -60.0))
(should (= (gallery-widget--needle-angle 150) 60.0)))
(ert-deftest gallery-widget-needle-angle-error-non-number ()
"A non-numeric value signals an error."
(should-error (gallery-widget--needle-angle "fifty"))
(should-error (gallery-widget--needle-angle nil)))
;;; --- rendered SVG structure ---
(defun test-gallery-widget--svg-string (value)
"Render the needle gauge at VALUE and return its XML string."
(gallery-widget-svg-string (gallery-widget-needle-gauge value)))
(ert-deftest gallery-widget-gauge-renders-svg-document ()
"The gauge renders to a parseable SVG document."
(let ((xml (test-gallery-widget--svg-string 42)))
(should (string-match-p "\\`<svg" xml))
(should (with-temp-buffer
(insert xml)
(libxml-parse-xml-region (point-min) (point-max))))))
(ert-deftest gallery-widget-gauge-has-expected-parts ()
"Arc, three ticks, needle, hub, and value text are all present."
(let ((xml (test-gallery-widget--svg-string 42)))
;; arc path stroked in the wash token
(should (string-match-p (format "path[^>]*stroke=\"%s\"" (gallery-widget-token 'wash)) xml))
;; exactly three ticks, counted as direct occurrences (the earlier
;; split-string arithmetic depended on omit-nulls edge behavior)
(should (= 3 (let ((n 0) (pos 0))
(while (string-match "class=\"tick\"" xml pos)
(setq n (1+ n) pos (match-end 0)))
n)))
;; needle + hub in the amber tokens
(should (string-match-p (format "class=\"needle\"[^>]*stroke=\"%s\""
(gallery-widget-token 'gold-hi))
xml))
;; hub is a half-dome sitting on the dial's bottom edge, like the web card
(should (string-match-p (format "class=\"hub\"[^>]*fill=\"%s\""
(gallery-widget-token 'gold))
xml))
(should (string-match-p "class=\"hub\"[^>]*d=\"M 44 48 A 4 4 0 0 1 52 48 Z\"" xml))
;; value readout
(should (string-match-p ">42%<" xml))))
(ert-deftest gallery-widget-gauge-has-glow-filter ()
"The needle glow is a real SVG blur filter, not a dropped effect."
(let ((xml (test-gallery-widget--svg-string 42)))
(should (string-match-p "feGaussianBlur" xml))
(should (string-match-p "filter=\"url(#" xml))))
(ert-deftest gallery-widget-gauge-needle-tracks-value ()
"The needle endpoint lands where the angle math says: left at 0, up at 50, right at 100."
;; value 50 -> vertical: x2 = pivot x (48), y2 = 48 - 40 = 8
(let ((xml (test-gallery-widget--svg-string 50)))
(should (string-match-p "class=\"needle\"[^>]*x2=\"48.0+\"" xml))
(should (string-match-p "class=\"needle\"[^>]*y2=\"8.0+\"" xml)))
;; value 100 -> +60 deg: x2 = 48 + 40*sin60 ~ 82.64
(should (string-match-p "x2=\"82.6" (test-gallery-widget--svg-string 100)))
;; value 0 -> -60 deg: x2 = 48 - 40*sin60 ~ 13.36
(should (string-match-p "x2=\"13.3" (test-gallery-widget--svg-string 0))))
(ert-deftest gallery-widget-gauge-integer-percent-in-readout ()
"The readout shows a rounded integer percent, matching the web card."
(should (string-match-p ">67%<" (test-gallery-widget--svg-string 66.6))))
(ert-deftest gallery-widget-gauge-out-of-range-readout-clamps ()
"Readout and needle agree at out-of-range values: both clamp.
Regression: the needle clamped to the dial ends while the readout rendered
the raw value, so a gauge fed 150 pinned the needle at +60 degrees under a
\"150%\" label."
(let ((over (test-gallery-widget--svg-string 150)))
(should (string-match-p ">100%<" over))
(should-not (string-match-p ">150%<" over)))
(let ((under (test-gallery-widget--svg-string -5)))
(should (string-match-p ">0%<" under))
(should-not (string-match-p ">-5%<" under))))
(ert-deftest gallery-widget-source-dir-load-and-fallback ()
"The token-file directory resolves from load context, else default-directory.
Regression: `file-name-directory' received nil when the load form was re-evaluated
outside a load and outside a file buffer."
(let ((load-file-name "/tmp/proto/gallery-widget.el"))
(should (equal (gallery-widget--source-dir) "/tmp/proto/")))
(with-temp-buffer
(let ((load-file-name nil)
(default-directory "/tmp/elsewhere/"))
(should (equal (gallery-widget--source-dir) "/tmp/elsewhere/")))))
(ert-deftest gallery-widget-write-svg-writes-file-and-returns-path ()
"The output helper writes the SVG document and returns the path."
(let ((file (make-temp-file "gallery-widget-test-" nil ".svg")))
(unwind-protect
(let ((ret (gallery-widget-write-svg
(gallery-widget-needle-gauge 42) file)))
(should (equal ret file))
(should (file-exists-p file))
(with-temp-buffer
(insert-file-contents file)
(should (string-match-p "\\`<svg" (buffer-string)))))
(delete-file file))))
(ert-deftest gallery-widget-requires-cl-lib-at-load-time ()
"Loading the module alone brings in cl-lib, not just an autoload cookie.
A cold byte-compile or a changed autoload would otherwise break
`gallery-widget--node' with a void-function error on first call."
(with-temp-buffer
(let ((emacs (expand-file-name invocation-name invocation-directory))
(widget (expand-file-name "docs/prototypes/gallery-widget.el"
test-gallery-widget--root)))
(call-process emacs nil t nil "--batch" "-l" widget
"--eval" "(princ (if (featurep 'cl-lib) \"cl-lib:yes\" \"cl-lib:no\"))")
(should (string-match-p "cl-lib:yes" (buffer-string))))))
(provide 'test-gallery-widget)
;;; test-gallery-widget.el ends here
|