ebox/tests/ebox-size-style-tests.el
Kinneyzhang 79f5bc23d1 feat: add CSS sizing and native text interaction capabilities
Normalize size units and intrinsic sizing across Elisp and native layout. Add help, pointer, hover-style and keymap support with reusable interaction adapters.

Keep content updates local, preserve scroll caches and hover borders, and avoid rebuilding retained plans and ownership metadata for stable geometry.

Validation: make check and native-rust-tests passed; targeted native interaction and scroll publication regressions passed.
2026-09-09 22:25:18 +08:00

224 lines
11 KiB
EmacsLisp

;;; ebox-size-style-tests.el --- CSS size style boundary tests -*- lexical-binding: t; -*-
;;; Code:
(require 'ert)
(require 'ebox-style)
(ert-deftest ebox-size-style-canonical-values ()
"Each size property accepts explicit lengths in its own rendering axis."
(dolist (case '(((:width :min-width :max-width)
(px 12) (% 50) (vw 33.5) (ch 80)
(calc (- (vw 100) (ch 1)))
(min (% 100) (ch 80))
(clamp (px 1) (% 50) (ch 80)))
((:height :min-height :max-height)
(lh 3) (vh 100) (% 50)
(calc (- (vh 100) (lh 1)))
(min (% 100) (lh 8))
(clamp (lh 1) (% 50) (vh 80)))))
(dolist (property (car case))
(dolist (value (cdr case))
(should (ebox-style-compile-form 'box (list property value)))))))
(ert-deftest ebox-size-style-rejects-legacy-values ()
"Legacy implicit lengths fail at every author dimension boundary."
(dolist (value '(12 0 (12) (0) viewport (viewport) viewport-height
(viewport-height) contain (fit-content (ch 80))))
(dolist (property '(:width :height :min-width :min-height
:max-width :max-height))
(should-error (ebox-style-compile-form 'box (list property value))))))
(ert-deftest ebox-size-style-keyword-applicability ()
"Maximum sizes accept none and reject auto on either axis."
(dolist (property '(:max-width :max-height))
(should-error (ebox-style-compile-form 'box (list property 'auto)))
(dolist (keyword '(none min-content max-content fit-content stretch))
(should (ebox-style-compile-form 'box (list property keyword)))))
(dolist (property '(:width :height :min-width :min-height))
(should-error (ebox-style-compile-form 'box (list property 'none)))))
(ert-deftest ebox-size-style-shorthands-preserve-atomic-expressions ()
"Shorthands distinguish one size expression from several edge values."
(let ((declarations (ebox-style-compile-form
'box '(:padding ((lh 1) (ch 2))
:margin ((lh 1) (ch 2))
:border ((px 1) solid "red")))))
(should (equal (plist-get declarations 'ebox/padding-block-start) '(lh 1)))
(should (equal (plist-get declarations 'ebox/padding-inline-end) '(ch 2)))
(should (equal (plist-get declarations 'ebox/margin-block-start) '(lh 1)))
(should (equal (plist-get declarations 'ebox/margin-inline-end) '(ch 2)))
(should (equal (plist-get declarations 'ebox/border-top-width) '(px 1)))))
(ert-deftest ebox-size-style-trbl-shorthand-arity ()
"Four-side shorthands keep arity rules and each target edge's unit domain."
(dolist (property '(:padding :margin :border-width))
(let ((unit (if (eq property :border-width) 'px '%)))
(dolist (amounts '((1) (1 2) (1 2 3) (1 2 3 4)))
(should (ebox-style-compile-form
'box (list property (mapcar (lambda (n) (list unit n))
amounts)))))))
(let ((declarations
(ebox-style-compile-form
'box '(:padding ((lh 1) (px 2) (lh 3) (px 4))))))
(should
(equal declarations
'(ebox/padding-block-start (lh 1)
ebox/padding-inline-end (px 2)
ebox/padding-block-end (lh 3)
ebox/padding-inline-start (px 4)))))
(dolist (property '(:padding :margin :border-width))
(dolist (values '(nil ((px 1) (px 2) (px 3) (px 4) (px 5))))
(should-error (ebox-style-compile-form 'box (list property values))))))
(ert-deftest ebox-size-style-pair-shorthand-arity ()
"Axis and gap shorthands preserve atomic expressions and target axes."
(dolist (case '((:padding-inline px) (:padding-block lh)
(:margin-inline px) (:margin-block lh) (:gap %)))
(dolist (amounts '((1) (1 2)))
(should (ebox-style-compile-form
'flex (list (car case)
(mapcar (lambda (n) (list (cadr case) n))
amounts))))))
(should
(equal (ebox-style-compile-form 'flex '(:gap ((lh 1) (ch 2))))
'(ebox/row-gap (lh 1) ebox/column-gap (ch 2))))
(let ((expression '(min (% 1) (% 2) (% 3) (% 4) (% 5))))
(should
(equal (ebox-style-compile-form 'flex (list :gap expression))
(list 'ebox/row-gap expression 'ebox/column-gap expression))))
(dolist (property '(:padding-inline :padding-block
:margin-inline :margin-block :gap))
(dolist (values '(nil ((px 1) (px 2) (px 3))))
(should-error (ebox-style-compile-form 'flex (list property values))))))
(ert-deftest ebox-size-style-property-ranges ()
"Each property enforces its supported size and percentage ranges."
(dolist (property '(:padding :gap :border-width :border-top-width))
(should-error (ebox-style-compile-form 'flex (list property 1))))
(should-error (ebox-style-compile-form 'box '(:padding (px -1))))
(should-error (ebox-style-compile-form 'box '(:margin (px -1))))
(should-error (ebox-style-compile-form 'box '(:border-width (% 2))))
(should-error (ebox-style-compile-form
'box '(:border-width (calc (+ (px 1) (% 2)))))))
(ert-deftest ebox-size-style-projection-remains-unresolved ()
"Computed projection preserves contextual sizes until used layout."
(let* ((expression '(calc (- (vh 100) (lh 1))))
(projected (ebox-style--expand-engine-plist
(list :height expression :width '(ch 80)
:padding-top '(lh 1) :margin-left '(ch 2)
:border-top-width '(px 1)
:border-top-style 'solid))))
(should (equal (plist-get projected :height) expression))
(should (equal (plist-get projected :width) '(ch 80)))
(should (equal (plist-get projected :padding-top-height) '(lh 1)))
(should (equal (plist-get projected :margin-left-pixel) '(ch 2)))
(should (equal (plist-get projected :border-top-pixel) '(px 1)))))
(ert-deftest ebox-size-style-generated-defaults ()
"Schema and generated declarations use canonical dimensional defaults."
(should (eq (plist-get (ebox-style-property :min-width) :initial) 'auto))
(should (eq (plist-get (ebox-style-property :min-height) :initial) 'auto))
(should (equal (plist-get (ebox-style-compile-form 'box '(:flex 1))
'ebox/flex-basis) '(% 0)))
(should (equal (plist-get (ebox-style-compile-form 'box '(:border "red"))
'ebox/border-top-width) '(px 1)))
(let ((declarations (ebox-style-compile-form 'box '(:border none))))
(should (equal (plist-get declarations 'ebox/border-top-width) '(px 1)))
(should (eq (plist-get declarations 'ebox/border-top-style) 'none)))
(should (equal (ebox-style--convert 0 'pixel) 0)))
(ert-deftest ebox-size-style-axis-matrix-is-enforced-during-build ()
"Dimensions, physical/logical edges, and gaps reject wrong-axis units early."
(require 'ebox)
(dolist (case
'((box inline
:width :min-width :max-width
:padding-right :padding-left :padding-inline-start :padding-inline-end
:margin-right :margin-left :margin-inline-start :margin-inline-end)
(box block
:height :min-height :max-height
:padding-top :padding-bottom :padding-block-start :padding-block-end
:margin-top :margin-bottom :margin-block-start :margin-block-end)
(flex inline :column-gap)
(flex block :row-gap)
(row inline :item-gap)
(column block :item-gap)))
(dolist (property (cddr case))
(dolist (unit '(px ch vw lh vh %))
(let ((form (list (car case) property (list unit 2))))
(ert-info ((format "initial build %S" form))
(if (memq unit (cdr (assq (cadr case) ebox-size-axis-units)))
(should (ebox-build form))
(let ((message
(condition-case condition
(progn (ebox-build form) nil)
(error (error-message-string condition)))))
(should (string-match-p (regexp-quote (symbol-name property))
message))
(should (string-match-p "allowed units:" message))))))))))
(ert-deftest ebox-size-style-recursive-axis-errors-reject-shorthands-and-tracks ()
"Expressions cannot hide forbidden units in shorthand or Grid wrappers."
(require 'ebox)
(dolist (form
'((box :height (calc (* 0 (px 1))))
(box :max-width (clamp (px 1) (lh 1) (ch 80)))
(box :padding ((calc (+ (lh 1) (px 0))) (ch 1)))
(box :margin-inline (min (ch 1) (vh 1)))
(box :padding (lh 1))
(box :margin (px 1))
(flex :gap (px 1))
(grid :grid-template-rows ((minmax (lh 1) (px 2))))
(grid :grid-template-columns ((repeat 2 (minmax (lh 1) (fr 1)))))))
(should-error (ebox-build form)))
(dolist (form
'((grid :grid-template-rows ((repeat 2 (minmax (lh 1) (fr 1)))))
(grid :grid-template-columns ((minmax (ch 1) (vw 20))))
(grid :grid-auto-rows (max (lh 1) (vh 20)))
(grid :grid-auto-columns (calc (+ (ch 1) (% 20))))))
(should (ebox-build form))))
(ert-deftest ebox-size-style-stroke-units-are-independent-of-edge-orientation ()
"All border edges retain their non-percentage paint-thickness domain."
(dolist (property '(:border-top-width :border-right-width
:border-bottom-width :border-left-width))
(dolist (unit '(px ch lh vw vh))
(should (ebox-style-compile-form 'box (list property (list unit 1)))))
(should-error (ebox-style-compile-form 'box (list property '(% 1))))))
(ert-deftest ebox-size-style-malformed-shorthand-functions-keep-unit-diagnostics ()
"Malformed atomic expressions report the receiving property's unit domain."
(dolist (case '((:gap (calc (lh 1) (lh 2) (lh 3)) ":row-gap")
(:padding (clamp (lh 1)) ":padding-block-start")
(:border ((calc (px 1) (px 2)) solid "red")
":border-top-width")))
(let ((message
(condition-case condition
(progn (ebox-style-compile-form 'flex (list (car case) (cadr case)))
nil)
(error (error-message-string condition)))))
(should (string-match-p (nth 2 case) message))
(should (string-match-p "allowed units:" message)))))
(ert-deftest ebox-size-style-simple-configs-enforce-axis-before-render ()
"Both direct LayoutConfig constructors enforce the same item-gap rules."
(require 'ebox-layout-config)
(should (equal (plist-get (ebox-layout-config-props
(ebox-row-layout-create)) :item-gap) '(px 0)))
(should (equal (plist-get (ebox-layout-config-props
(ebox-column-layout-create)) :item-gap) '(lh 0)))
(should-error (ebox-row-layout-create :item-gap '(lh 1))
:type 'ebox-size-error)
(should-error (ebox-column-layout-create :item-gap '(px 0))
:type 'ebox-size-error)
(should-error
(ebox-layout-config-with-props
(ebox-column-layout-create)
'(:item-gap (calc (* 0 (px 1))) :cross-align stretch))
:type 'ebox-size-error))
(provide 'ebox-size-style-tests)
;;; ebox-size-style-tests.el ends here