diff options
Diffstat (limited to 'tests/test-build-theme.el')
| -rw-r--r-- | tests/test-build-theme.el | 293 |
1 files changed, 246 insertions, 47 deletions
diff --git a/tests/test-build-theme.el b/tests/test-build-theme.el index 87b17e0a4..8793da73a 100644 --- a/tests/test-build-theme.el +++ b/tests/test-build-theme.el @@ -1,4 +1,4 @@ -;;; test-build-theme.el --- Tests for the theme.json -> dupre-*.el converter -*- lexical-binding: t -*- +;;; test-build-theme.el --- Tests for the theme.json -> deftheme converter -*- lexical-binding: t -*- ;;; Commentary: @@ -34,12 +34,14 @@ "{ \"name\": \"dupre-fixture\", \"palette\": [[\"#000000\",\"ground\"],[\"#7a9abe\",\"blue\"],[\"#84b068\",\"green\"]], - \"assignments\": { - \"bg\":\"#000000\", \"p\":\"#cdced1\", - \"kw\":\"#7a9abe\", \"str\":\"#84b068\", \"cm\":\"#838d97\", \"dec\":\"#e8bd30\" + \"syntax\": { + \"bg\": {\"fg\":\"#000000\",\"bg\":null,\"bold\":false,\"italic\":false}, + \"p\": {\"fg\":\"#cdced1\",\"bg\":null,\"bold\":false,\"italic\":false}, + \"kw\": {\"fg\":\"#7a9abe\",\"bg\":null,\"bold\":true,\"italic\":false}, + \"str\":{\"fg\":\"#84b068\",\"bg\":null,\"bold\":false,\"italic\":false}, + \"cm\": {\"fg\":\"#838d97\",\"bg\":null,\"bold\":false,\"italic\":true}, + \"dec\":{\"fg\":\"#e8bd30\",\"bg\":null,\"bold\":false,\"italic\":false} }, - \"bold\": [\"kw\"], - \"italic\": [\"cm\"], \"ui\": { \"region\": {\"fg\":null, \"bg\":\"#264364\"}, \"mode-line\": {\"fg\":\"#cdced1\", \"bg\":\"#2f343a\"} @@ -54,8 +56,10 @@ }" "A self-contained theme.json exercising every tier: default, syntax (bold + italic + the unmappable dec key), UI, and packages (a plain face, an -inherit+height face, and a cleared face). Owned by the test so it can't drift -the way Craig's downloaded exports under scripts/theme-studio/ can.") +inherit+height face, and a cleared face). Uses the nested \"syntax\" format the +converter reads -- each category is an object with fg/bg/bold/italic, and bg/p +are themselves category objects carrying fg. Owned by the test so it can't +drift the way Craig's downloaded exports under scripts/theme-studio/ can.") (defun test-build-theme--write-fixture (dir) "Write the fixture JSON into DIR and return its path." @@ -70,7 +74,7 @@ the way Craig's downloaded exports under scripts/theme-studio/ can.") (unwind-protect (progn ,@body) (delete-directory ,var t)))) -;; --- WCAG contrast helpers (mirror of the dupre-theme test helpers) --- +;; --- WCAG contrast helpers --- (defun test-build-theme--channel-luminance (c) "Linearize an 8-bit channel value C (0-255) per the WCAG formula." @@ -91,43 +95,175 @@ the way Craig's downloaded exports under scripts/theme-studio/ can.") ;;; --------------------------------------------------------------------------- ;;; build-theme/--attrs (the core attribute builder) +;; +;; `--attrs' takes one face-spec alist and emits a face-attribute plist. It +;; reads the full attribute model and tolerates the legacy boolean +;; bold/italic/underline/strike fields that older theme.json exports carry. -(ert-deftest test-build-theme-attrs-fg-and-bold () - "Normal: a foreground plus bold yields :foreground and :weight bold." - (should (equal (build-theme/--attrs nil "#67809c" nil t nil nil nil nil) +;; --- Legacy boolean fields still work (back-compat with committed presets) --- + +(ert-deftest test-build-theme-attrs-legacy-fg-and-bold () + "Normal: legacy bold flag yields :weight bold." + (should (equal (build-theme/--attrs '((fg . "#67809c") (bold . t))) '(:foreground "#67809c" :weight bold)))) -(ert-deftest test-build-theme-attrs-full-ordering () - "Normal: every attribute present, in canonical order." - (should (equal (build-theme/--attrs 'org-level-1 "#e8bd30" "#1a1714" t t t t 1.3) - '(:inherit org-level-1 :foreground "#e8bd30" :background "#1a1714" - :weight bold :slant italic :underline t :strike-through t :height 1.3)))) - -(ert-deftest test-build-theme-attrs-underline-and-strike () - "Normal: underline and strike yield :underline t and :strike-through t." - (should (equal (build-theme/--attrs nil "#67809c" nil nil nil t t nil) - '(:foreground "#67809c" :underline t :strike-through t))) - ;; either alone - (should (equal (build-theme/--attrs nil nil nil nil nil t nil nil) - '(:underline t))) - (should (equal (build-theme/--attrs nil nil nil nil nil nil t nil) - '(:strike-through t)))) +(ert-deftest test-build-theme-attrs-legacy-italic-underline-strike () + "Normal: legacy italic/underline/strike booleans map to their attributes." + (should (equal (build-theme/--attrs '((italic . t))) '(:slant italic))) + (should (equal (build-theme/--attrs '((underline . t))) '(:underline t))) + (should (equal (build-theme/--attrs '((strike . t))) '(:strike-through t)))) (ert-deftest test-build-theme-attrs-empty-is-nil () - "Boundary: a fully-cleared face (all nil) yields an empty plist." - (should (equal (build-theme/--attrs nil nil nil nil nil nil nil nil) '()))) + "Boundary: a blank face (empty alist, or all-nil fields) yields an empty plist." + (should (equal (build-theme/--attrs '()) '())) + (should (equal (build-theme/--attrs '((fg) (bg) (bold) (italic) (underline) (strike))) '()))) (ert-deftest test-build-theme-attrs-bold-false-omits-weight () - "Boundary: bold false produces no :weight key (only overrides are written)." - (should (equal (build-theme/--attrs nil "#cdced1" nil nil nil nil nil nil) - '(:foreground "#cdced1")))) + "Boundary: bold false (or absent) writes no :weight -- only overrides appear." + (should (equal (build-theme/--attrs '((fg . "#cdced1") (bold . nil))) + '(:foreground "#cdced1"))) + (should (equal (build-theme/--attrs '((fg . "#cdced1"))) '(:foreground "#cdced1")))) (ert-deftest test-build-theme-attrs-height-one-omitted () - "Boundary: a height of exactly 1.0 is omitted (the default multiplier)." - (should (equal (build-theme/--attrs nil "#cdced1" nil nil nil nil nil 1.0) - '(:foreground "#cdced1"))) - (should (equal (build-theme/--attrs nil "#cdced1" nil nil nil nil nil 1) - '(:foreground "#cdced1")))) + "Boundary: a height of exactly 1.0 (or integer 1) is omitted as the default." + (should (equal (build-theme/--attrs '((fg . "#cdced1") (height . 1.0))) '(:foreground "#cdced1"))) + (should (equal (build-theme/--attrs '((fg . "#cdced1") (height . 1))) '(:foreground "#cdced1"))) + (should (equal (build-theme/--attrs '((height . 1.2))) '(:height 1.2)))) + +;; --- New attributes --- + +(ert-deftest test-build-theme-attrs-family () + "Normal/Boundary: a non-empty family string emits :family; empty is omitted." + (should (equal (build-theme/--attrs '((family . "Iosevka"))) '(:family "Iosevka"))) + (should (equal (build-theme/--attrs '((family . ""))) '())) + (should (equal (build-theme/--attrs '((family . nil))) '()))) + +(ert-deftest test-build-theme-attrs-distant-foreground () + "Normal: distant-fg emits :distant-foreground." + (should (equal (build-theme/--attrs '((distant-fg . "#ffffff"))) + '(:distant-foreground "#ffffff")))) + +(ert-deftest test-build-theme-attrs-weight-range () + "Normal: an explicit weight string emits that weight symbol." + (should (equal (build-theme/--attrs '((weight . "light"))) '(:weight light))) + (should (equal (build-theme/--attrs '((weight . "semibold"))) '(:weight semibold))) + (should (equal (build-theme/--attrs '((weight . "heavy"))) '(:weight heavy)))) + +(ert-deftest test-build-theme-attrs-weight-overrides-legacy-bold () + "Boundary: an explicit weight wins over a legacy bold flag on the same face." + (should (equal (build-theme/--attrs '((weight . "light") (bold . t))) + '(:weight light)))) + +(ert-deftest test-build-theme-attrs-slant-range () + "Normal: an explicit slant string emits that slant; it wins over legacy italic." + (should (equal (build-theme/--attrs '((slant . "oblique"))) '(:slant oblique))) + (should (equal (build-theme/--attrs '((slant . "normal"))) '(:slant normal))) + (should (equal (build-theme/--attrs '((slant . "oblique") (italic . t))) '(:slant oblique)))) + +(ert-deftest test-build-theme-attrs-underline-object () + "Normal/Boundary: the structured underline form covers line/wave and color." + ;; plain line in the face color collapses to t + (should (equal (build-theme/--attrs '((underline . ((style . "line") (color . nil))))) + '(:underline t))) + ;; wave alone -> a :style plist + (should (equal (build-theme/--attrs '((underline . ((style . "wave") (color . nil))))) + '(:underline (:style wave)))) + ;; colored line -> a :color plist + (should (equal (build-theme/--attrs '((underline . ((style . "line") (color . "#cb6b4d"))))) + '(:underline (:color "#cb6b4d")))) + ;; colored wave -> both + (should (equal (build-theme/--attrs '((underline . ((style . "wave") (color . "#cb6b4d"))))) + '(:underline (:color "#cb6b4d" :style wave))))) + +(ert-deftest test-build-theme-attrs-strike-object () + "Normal: structured strike emits t for no color, or the color string." + (should (equal (build-theme/--attrs '((strike . ((color . nil))))) '(:strike-through t))) + (should (equal (build-theme/--attrs '((strike . ((color . "#cb6b4d"))))) + '(:strike-through "#cb6b4d")))) + +(ert-deftest test-build-theme-attrs-migrated-shapes-match-legacy () + "Boundary: the shapes the import migration produces emit identically to the +legacy booleans they replace, so the cutover keeps generated themes byte-identical. +Mirrors migrateLegacyFace (app-core.js) / migrate_legacy (face_specs.py)." + (should (equal (build-theme/--attrs '((weight . "bold"))) + (build-theme/--attrs '((bold . t))))) + (should (equal (build-theme/--attrs '((slant . "italic"))) + (build-theme/--attrs '((italic . t))))) + (should (equal (build-theme/--attrs '((underline . ((style . "line") (color . nil))))) + (build-theme/--attrs '((underline . t))))) + (should (equal (build-theme/--attrs '((strike . ((color . nil))))) + (build-theme/--attrs '((strike . t)))))) + +(ert-deftest test-build-theme-attrs-overline () + "Normal/Boundary: overline emits t for no color, the color otherwise, nil when unset." + (should (equal (build-theme/--attrs '((overline . ((color . nil))))) '(:overline t))) + (should (equal (build-theme/--attrs '((overline . ((color . "#a9b2bb"))))) + '(:overline "#a9b2bb"))) + (should (equal (build-theme/--attrs '((overline . nil))) '()))) + +(ert-deftest test-build-theme-attrs-inverse-and-extend () + "Normal/Boundary: inverse and extend emit t when set, nothing when nil." + (should (equal (build-theme/--attrs '((inverse . t))) '(:inverse-video t))) + (should (equal (build-theme/--attrs '((extend . t))) '(:extend t))) + (should (equal (build-theme/--attrs '((inverse . t) (extend . t))) + '(:inverse-video t :extend t))) + (should (equal (build-theme/--attrs '((inverse . nil) (extend . nil))) '()))) + +(ert-deftest test-build-theme-attrs-inherit-any-tier () + "Normal: inherit coerces a face-name string to a symbol (now allowed on every tier)." + (should (equal (build-theme/--attrs '((inherit . "shadow"))) '(:inherit shadow))) + (should (equal (build-theme/--attrs '((inherit . shadow))) '(:inherit shadow))) + (should (equal (build-theme/--attrs '((inherit . nil))) '()))) + +(ert-deftest test-build-theme-attrs-full-ordering () + "Normal: every attribute present, emitted in canonical order." + (should (equal (build-theme/--attrs + '((inherit . "org-level-1") (family . "Iosevka") + (fg . "#e8bd30") (bg . "#1a1714") (distant-fg . "#ffffff") + (weight . "semibold") (slant . "italic") (height . 1.3) + (underline . ((style . "wave") (color . "#cb6b4d"))) + (overline . ((color . "#a9b2bb"))) + (strike . ((color . nil))) + (box . ((style . "line") (color . "#67809c"))) + (inverse . t) (extend . t))) + '(:inherit org-level-1 :family "Iosevka" + :foreground "#e8bd30" :background "#1a1714" :distant-foreground "#ffffff" + :weight semibold :slant italic :height 1.3 + :underline (:color "#cb6b4d" :style wave) :overline "#a9b2bb" + :strike-through t :box (:line-width 1 :color "#67809c") + :inverse-video t :extend t)))) + +;; --- Attribute-helper edge cases (the coercion functions in isolation) --- + +(ert-deftest test-build-theme-weight-helper () + "Boundary: weight prefers explicit string, falls back to bold, else nil." + (should (eq (build-theme/--weight '((weight . "bold"))) 'bold)) + (should (eq (build-theme/--weight '((weight . "light") (bold . t))) 'light)) + (should (eq (build-theme/--weight '((bold . t))) 'bold)) + (should (null (build-theme/--weight '((weight . "") (bold . nil))))) + (should (null (build-theme/--weight '())))) + +(ert-deftest test-build-theme-slant-helper () + "Boundary: slant prefers explicit string, falls back to italic, else nil." + (should (eq (build-theme/--slant '((slant . "oblique"))) 'oblique)) + (should (eq (build-theme/--slant '((italic . t))) 'italic)) + (should (null (build-theme/--slant '((slant . ""))))) + (should (null (build-theme/--slant '())))) + +(ert-deftest test-build-theme-underline-helper () + "Boundary: underline coercion across nil / legacy t / structured forms." + (should (null (build-theme/--underline '((underline . nil))))) + (should (eq (build-theme/--underline '((underline . t))) t)) + (should (eq (build-theme/--underline '((underline . ((style . "line") (color . nil))))) t)) + (should (equal (build-theme/--underline '((underline . ((style . "wave"))))) '(:style wave))) + (should (equal (build-theme/--underline '((underline . ((color . "#aa0000"))))) '(:color "#aa0000")))) + +(ert-deftest test-build-theme-line-attr-helper () + "Boundary: the overline/strike coercion: nil / t / {color} forms." + (should (null (build-theme/--line-attr nil))) + (should (eq (build-theme/--line-attr t) t)) + (should (eq (build-theme/--line-attr '((color . nil))) t)) + (should (equal (build-theme/--line-attr '((color . "#abcdef"))) "#abcdef"))) ;;; --------------------------------------------------------------------------- ;;; build-theme/--face-spec (skips empty faces) @@ -145,9 +281,11 @@ the way Craig's downloaded exports under scripts/theme-studio/ can.") ;;; Syntax tier (ert-deftest test-build-theme-syntax-keyword-bold () - "Normal: kw maps to font-lock-keyword-face and picks up the bold set." - (let* ((assignments '((kw . "#7a9abe") (str . "#84b068"))) - (specs (build-theme/--syntax-face-specs assignments '(kw) '()))) + "Normal: kw maps to font-lock-keyword-face and carries its bold flag. +Each syntax category is a nested object with fg/bold/italic." + (let* ((syntax '((kw . ((fg . "#7a9abe") (bold . t))) + (str . ((fg . "#84b068"))))) + (specs (build-theme/--syntax-face-specs syntax))) (should (member '(font-lock-keyword-face ((t (:foreground "#7a9abe" :weight bold)))) specs)) (should (member '(font-lock-string-face ((t (:foreground "#84b068")))) @@ -155,7 +293,7 @@ the way Craig's downloaded exports under scripts/theme-studio/ can.") (ert-deftest test-build-theme-syntax-one-to-many () "Normal: punc fans out to every punctuation/bracket/delimiter face." - (let ((specs (build-theme/--syntax-face-specs '((punc . "#a9b2bb")) '() '()))) + (let ((specs (build-theme/--syntax-face-specs '((punc . ((fg . "#a9b2bb"))))))) (dolist (face '(font-lock-punctuation-face font-lock-bracket-face font-lock-delimiter-face font-lock-misc-punctuation-face)) (should (member `(,face ((t (:foreground "#a9b2bb")))) specs))))) @@ -164,12 +302,12 @@ the way Craig's downloaded exports under scripts/theme-studio/ can.") "Boundary: dec has no independent Emacs face, so it maps to nothing. Emacs renders decorators with font-lock-type-face, which ty already owns; mapping dec would clobber the type color." - (let ((specs (build-theme/--syntax-face-specs '((dec . "#e8bd30")) '() '()))) + (let ((specs (build-theme/--syntax-face-specs '((dec . ((fg . "#e8bd30"))))))) (should (null specs)))) (ert-deftest test-build-theme-syntax-comment-italic () - "Normal: cm in the italic set yields :slant italic on the comment face." - (let ((specs (build-theme/--syntax-face-specs '((cm . "#a9b2bb")) '() '(cm)))) + "Normal: cm with its italic flag yields :slant italic on the comment face." + (let ((specs (build-theme/--syntax-face-specs '((cm . ((fg . "#a9b2bb") (italic . t))))))) (should (member '(font-lock-comment-face ((t (:foreground "#a9b2bb" :slant italic)))) specs)))) @@ -177,8 +315,9 @@ mapping dec would clobber the type color." ;;; Default face (ert-deftest test-build-theme-default-face () - "Normal: default takes background from bg and foreground from p." - (should (equal (build-theme/--default-spec '((bg . "#000000") (p . "#cdced1"))) + "Normal: default takes background from syntax.bg.fg and foreground from syntax.p.fg." + (should (equal (build-theme/--default-spec '((bg . ((fg . "#000000"))) + (p . ((fg . "#cdced1"))))) '(default ((t (:foreground "#cdced1" :background "#000000"))))))) ;;; --------------------------------------------------------------------------- @@ -294,7 +433,7 @@ including an inherit+height package face." (ert-deftest test-build-theme-convert-file-old-json-without-packages () "Boundary: a theme.json with no packages key still converts and loads." (test-build-theme--with-sandbox out - (let* ((json "{\"name\":\"noformat\",\"palette\":[[\"#000000\",\"ground\"]],\"assignments\":{\"bg\":\"#000000\",\"p\":\"#ffffff\",\"kw\":\"#67809c\"},\"bold\":[\"kw\"],\"italic\":[],\"ui\":{}}") + (let* ((json "{\"name\":\"noformat\",\"palette\":[[\"#000000\",\"ground\"]],\"syntax\":{\"bg\":{\"fg\":\"#000000\"},\"p\":{\"fg\":\"#ffffff\"},\"kw\":{\"fg\":\"#67809c\",\"bold\":true}},\"ui\":{}}") (in (expand-file-name "noformat.json" out))) (with-temp-file in (insert json)) (let ((path (build-theme/convert-file in out))) @@ -313,6 +452,25 @@ including an inherit+height package face." (test-build-theme--with-sandbox out (should-error (build-theme/convert-file (expand-file-name "does-not-exist.json" out) out)))) +(ert-deftest test-build-theme-name-from-filename-not-json-field () + "Normal/Regression: the output name comes from the JSON file's basename, not +its internal name field, so each draft exports under its own name (a WIP.json +becomes WIP-theme.el, never theme-theme.el)." + (test-build-theme--with-sandbox out + ;; The fixture's internal name field is \"dupre-fixture\"; the file is sterling.json. + (let ((in (expand-file-name "sterling.json" out))) + (with-temp-file in (insert test-build-theme--fixture-json)) + (let ((path (build-theme/convert-file in out))) + (should (string-suffix-p "sterling-theme.el" path)) + (should-not (string-match-p "dupre-fixture" path)) + (let ((custom-theme-load-path (cons out custom-theme-load-path)) + (load-path (cons out load-path))) + (unwind-protect + (progn + (load-theme 'sterling t) + (should (string= (face-attribute 'default :background nil t) "#000000"))) + (disable-theme 'sterling))))))) + (ert-deftest test-build-theme-generated-default-meets-wcag-aa () "Error/Regression: the generated default face stays legible. A WCAG-AA (>= 4.5:1) assertion on the round-tripped result -- proves the whole @@ -329,5 +487,46 @@ parse -> spec -> file -> face pipeline preserves the designed contrast." (should (>= (test-build-theme--contrast fg bg) 4.5)))) (disable-theme 'dupre-fixture)))))) +(ert-deftest test-build-theme-convert-file-new-attributes-round-trip () + "Integration: the new attribute model survives parse -> spec -> file -> face. +Components integrated: +- build-theme/convert-file (entry point, real) +- json parsing of the inline fixture (real) +- custom-theme-set-faces / load-theme / face-attribute (real) +Exercises extend, structured underline (wave + color), overline, inverse-video, +distant-foreground, family, and the weight/slant ranges across the UI and +package tiers." + (test-build-theme--with-sandbox out + (let* ((json "{\"name\":\"newattrs\",\"palette\":[[\"#000000\",\"ground\"]], + \"syntax\":{\"bg\":{\"fg\":\"#000000\"},\"p\":{\"fg\":\"#ffffff\"}}, + \"ui\":{ + \"region\":{\"bg\":\"#264364\",\"extend\":true}, + \"highlight\":{\"fg\":\"#eddba7\",\"underline\":{\"style\":\"wave\",\"color\":\"#cb6b4d\"},\"overline\":{\"color\":\"#a9b2bb\"}}, + \"secondary-selection\":{\"bg\":\"#333333\",\"inverse\":true,\"distant-fg\":\"#ffffff\"} + }, + \"packages\":{ + \"misc\":{ + \"shadow\":{\"fg\":\"#cdced1\",\"family\":\"Iosevka\",\"weight\":\"light\",\"slant\":\"oblique\",\"source\":\"user\"} + } + }}") + (in (expand-file-name "newattrs.json" out))) + (with-temp-file in (insert json)) + (build-theme/convert-file in out) + (let ((custom-theme-load-path (cons out custom-theme-load-path)) + (load-path (cons out load-path))) + (unwind-protect + (progn + (load-theme 'newattrs t) + (should (eq (face-attribute 'region :extend nil t) t)) + (should (equal (face-attribute 'highlight :underline nil t) + '(:color "#cb6b4d" :style wave))) + (should (string= (face-attribute 'highlight :overline nil t) "#a9b2bb")) + (should (eq (face-attribute 'secondary-selection :inverse-video nil t) t)) + (should (string= (face-attribute 'secondary-selection :distant-foreground nil t) "#ffffff")) + (should (string= (face-attribute 'shadow :family nil t) "Iosevka")) + (should (eq (face-attribute 'shadow :weight nil t) 'light)) + (should (eq (face-attribute 'shadow :slant nil t) 'oblique))) + (disable-theme 'newattrs)))))) + (provide 'test-build-theme) ;;; test-build-theme.el ends here |
