945 lines
44 KiB
EmacsLisp
945 lines
44 KiB
EmacsLisp
;;; ebox-dsl-tests.el --- Ebox DSL tests -*- lexical-binding: t; -*-
|
|
|
|
(require 'ert)
|
|
|
|
(load-file (expand-file-name "../ebox.el" (file-name-directory (or load-file-name buffer-file-name))))
|
|
|
|
(defun ebox-dsl-test--plain (node)
|
|
"Render NODE and strip text properties."
|
|
(substring-no-properties (ebox-render node)))
|
|
|
|
(defun ebox-dsl-test--line-widths (node)
|
|
"Return rendered pixel widths for NODE line by line."
|
|
(mapcar #'ebox--string-pixel-width (ebox-string-lines (ebox-render node))))
|
|
|
|
(defun ebox-dsl-test--display-line-widths (node)
|
|
"Return rendered display pixel widths for NODE line by line."
|
|
(mapcar #'ebox--string-pixel-width (ebox-string-lines (ebox-render node))))
|
|
|
|
(defun ebox-dsl-test--tree-count (node predicate)
|
|
"Count nodes below NODE for which PREDICATE returns non-nil."
|
|
(if (or (stringp node) (not (listp node)))
|
|
0
|
|
(+ (if (funcall predicate node) 1 0)
|
|
(cl-loop for child in (ebox-tree-node-children node)
|
|
sum (ebox-dsl-test--tree-count child predicate)))))
|
|
|
|
(ert-deftest ebox-style-expands-ebox-aliases-to-canonical-longhands ()
|
|
"Existing Ebox aliases should normalize to CSS-like longhand properties."
|
|
(let ((style (ebox-style-compute
|
|
(list :bgcolor "#ffffff"
|
|
:padding '(1 2)
|
|
:margin-left 3
|
|
:border "#333333"))))
|
|
(should (equal (plist-get style :background-color) "#ffffff"))
|
|
(should (= (plist-get style :padding-block-start) 1))
|
|
(should (= (plist-get style :padding-inline-end) 2))
|
|
(should (= (plist-get style :padding-block-end) 1))
|
|
(should (= (plist-get style :padding-inline-start) 2))
|
|
(should (= (plist-get style :margin-inline-start) 3))
|
|
(should (equal (plist-get style :border-color) "#333333"))))
|
|
|
|
(ert-deftest ebox-style-classifies-incremental-effects ()
|
|
"Style metadata should classify paint and layout effects."
|
|
(should (eq (ebox-style-dirty-kind :background-color) 'paint))
|
|
(should (eq (ebox-style-dirty-kind :border-color) 'paint))
|
|
(should (eq (ebox-style-dirty-kind :padding-inline-start) 'geometry))
|
|
(should (eq (ebox-style-dirty-kind :width) 'geometry))
|
|
(should (eq (ebox-style-dirty-kind :outer) 'structure))
|
|
(should (eq (ebox-style-dirty-kind :layout) 'structure))
|
|
(should (eq (ebox-style-dirty-kind :display) 'structure)))
|
|
|
|
(ert-deftest ebox-style-projects-public-box-axes-to-internal-display ()
|
|
"Computed outer/layout properties should project to one display pair."
|
|
(let* ((style
|
|
(ebox-style-compute-subject
|
|
(ecss-subject-create :type "box")
|
|
(ebox-style-compile-declarations
|
|
'(:outer inline :layout flex))))
|
|
(node (ebox-build '(box "A"))))
|
|
(ebox-style-apply-computed node style)
|
|
(should (equal (ebox--computed-display node) '(inline flex)))))
|
|
|
|
(ert-deftest ebox-style-strict-cache-does-not-hide-unknown-properties ()
|
|
"Strict declaration validation should remain exact after an empty cache hit."
|
|
(ebox-style-compile-declarations nil t)
|
|
(should-error
|
|
(ebox-style-compile-declarations '(:not-an-ebox-property 1) t)
|
|
:type 'error))
|
|
|
|
(ert-deftest ebox-style-aliases-share-one-canonical-property-id ()
|
|
"A concise alias should disappear before canonical declarations."
|
|
(should
|
|
(equal (ebox-style-compile-declarations '(:bgcolor "red") t)
|
|
(ebox-style-compile-declarations '(:background-color "red") t))))
|
|
|
|
(ert-deftest ebox-style-rejects-canonical-and-alias-duplicates ()
|
|
"One declaration source should not spell one property twice."
|
|
(should-error
|
|
(ebox-style-compile-declarations
|
|
'(:bgcolor "red" :background-color "blue") t)
|
|
:type 'error)
|
|
(should-error
|
|
(ebox-style-compile-declarations
|
|
'(:background-color "red" :bgcolor "blue") t)
|
|
:type 'error))
|
|
|
|
(ert-deftest ebox-style-property-registry-rejects-alias-collisions ()
|
|
"An alias should never shadow another canonical property."
|
|
(let ((ebox-style--property-table nil)
|
|
(ebox-style--property-definitions
|
|
'((:name :first :id ebox/first :aliases (:second))
|
|
(:name :second :id ebox/second))))
|
|
(should-error (ebox-style--ensure-property-table) :type 'error)))
|
|
|
|
(ert-deftest ebox-style-border-present-is-a-typed-shorthand-not-an-alias ()
|
|
"The concise border boolean should expand to canonical width and style."
|
|
(should
|
|
(equal (ebox-style-compile-declarations '(:border-top-p t) t)
|
|
'(ebox/border-top-width 1 ebox/border-top-style solid)))
|
|
(should
|
|
(equal (ebox-style-compile-declarations '(:border-top-p nil) t)
|
|
'(ebox/border-top-width 0 ebox/border-top-style none)))
|
|
(should (eq (ebox-style-canonical-name :border-top-p) :border-top-p)))
|
|
|
|
(ert-deftest ebox-style-exclusive-check-does-not-reexpand-ordinary-shorthands ()
|
|
"Conflict validation should leave the declaration cache effective."
|
|
(let ((calls 0)
|
|
(original (symbol-function 'ebox-style--padding-shorthand)))
|
|
(clrhash ebox-style--declaration-cache)
|
|
(cl-letf (((symbol-function 'ebox-style--padding-shorthand)
|
|
(lambda (value)
|
|
(cl-incf calls)
|
|
(funcall original value))))
|
|
(ebox-style-compile-declarations '(:padding 1) t)
|
|
(ebox-style-compile-declarations '(:padding 1) t))
|
|
(should (= calls 1))))
|
|
|
|
(ert-deftest ebox-style-border-present-rejects-owned-longhand-conflicts ()
|
|
"One source should not mix the boolean border shorthand with its outputs."
|
|
(should-error
|
|
(ebox-style-compile-declarations
|
|
'(:border-top-p t :border-top-width 2) t)
|
|
:type 'error)
|
|
(should-error
|
|
(ebox-style-compile-declarations
|
|
'(:border-top-style dashed :border-top-p t) t)
|
|
:type 'error)
|
|
(should-error
|
|
(ebox-style-compile-declarations
|
|
'(:border-top-p t :border-top (2 dashed "red")) t)
|
|
:type 'error)
|
|
(should-error
|
|
(ebox-style-compile-declarations
|
|
'(:border-top-p t :border-width 2) t)
|
|
:type 'error)
|
|
(should-error
|
|
(ebox-style-compile-declarations
|
|
'(:border-style dashed :border-top-p t) t)
|
|
:type 'error)
|
|
(should-error
|
|
(ebox-style-compile-declarations
|
|
'(:border-bottom-p t :border (1 solid "red")) t)
|
|
:type 'error))
|
|
|
|
(ert-deftest ebox-canonical-text-constructor-is-renderable-and-typed ()
|
|
"A canonical TextNode should retain its kind, source, and string value."
|
|
(let ((node (ebox-text-create :value "Hello" :source-handle 'source-text)))
|
|
(should (ebox-text-node-p node))
|
|
(should-not (ebox-box-node-p node))
|
|
(should (eq (ebox-node-source-handle node) 'source-text))
|
|
(should (string= (ebox-text-node-value node) "Hello"))
|
|
(should (string= (ebox-dsl-test--plain node) "Hello"))))
|
|
|
|
(ert-deftest ebox-canonical-text-constructor-rejects-non-text-shapes ()
|
|
"A canonical TextNode should have exactly one string payload."
|
|
(should-error (ebox-text-create :value 7) :type 'error)
|
|
(should-error (ebox-text-create :value "A" :content "B") :type 'error)
|
|
(should-error (ebox-text-create :value "A" :children nil) :type 'error)
|
|
(should-error (ebox-text-create :value "A" :color "red") :type 'error)
|
|
(should-error
|
|
(ebox-text-create :value "A" :value "B")
|
|
:type 'error)
|
|
(should-error
|
|
(ebox-text-create :value "A" :source-handle 'a :source-handle 'b)
|
|
:type 'error))
|
|
|
|
(ert-deftest ebox-canonical-normal-box-renders-one-typed-text-child ()
|
|
"A canonical BoxNode should retain typed Layout and child source facts."
|
|
(let* ((text (ebox-text-create :value "A" :source-handle 'source-text))
|
|
(layout (ebox-normal-layout-create))
|
|
(box (ebox-box-create :layout layout
|
|
:children (list text)
|
|
:source-handle 'source-box)))
|
|
(should (ebox-box-node-p box))
|
|
(should-not (ebox-text-node-p box))
|
|
(should (eq (ebox-node-source-handle box) 'source-box))
|
|
(should (eq (ebox-layout-config-kind
|
|
(ebox-box-node-layout box))
|
|
'normal))
|
|
(should (equal (ebox-box-node-children box) (list text)))
|
|
(should (string= (ebox-dsl-test--plain box) "A"))))
|
|
|
|
(ert-deftest ebox-canonical-row-box-retains-and-renders-typed-children ()
|
|
"A Row Box should retain one Box identity around all typed children."
|
|
(let* ((left (ebox-text-create :value "A" :source-handle 'left))
|
|
(right (ebox-text-create :value "B" :source-handle 'right))
|
|
(box (ebox-box-create :layout (ebox-row-layout-create)
|
|
:children (list left right)
|
|
:source-handle 'row)))
|
|
(should (ebox-box-node-p box))
|
|
(should (eq (ebox-node-source-handle box) 'row))
|
|
(should (eq (ebox-layout-config-kind (ebox-box-node-layout box)) 'row))
|
|
(should (equal (ebox-box-node-children box) (list left right)))
|
|
(should (eq (ebox-tree-node-children box)
|
|
(ebox-box-node-children box)))
|
|
(should (= (ebox-dsl-test--tree-count box (lambda (_node) t)) 3))
|
|
(should (= (ebox-dsl-test--tree-count box #'ebox-node-kind) 3))
|
|
(should-not (plist-member box :ebox-content-node))
|
|
(should (equal (ebox--computed-display box) '(block row)))
|
|
(should (string= (ebox-dsl-test--plain box) "AB"))))
|
|
|
|
(ert-deftest ebox-canonical-column-box-retains-and-renders-typed-children ()
|
|
"A Column Box should retain one Box identity around all typed children."
|
|
(let* ((top (ebox-text-create :value "A"))
|
|
(bottom (ebox-text-create :value "B"))
|
|
(box (ebox-box-create :layout (ebox-column-layout-create)
|
|
:children (list top bottom))))
|
|
(should (ebox-box-node-p box))
|
|
(should (eq (ebox-layout-config-kind (ebox-box-node-layout box)) 'column))
|
|
(should (equal (ebox-box-node-children box) (list top bottom)))
|
|
(should (eq (ebox-tree-node-children box)
|
|
(ebox-box-node-children box)))
|
|
(should (= (ebox-dsl-test--tree-count box (lambda (_node) t)) 3))
|
|
(should (= (ebox-dsl-test--tree-count box #'ebox-node-kind) 3))
|
|
(should-not (plist-member box :ebox-content-node))
|
|
(should (equal (ebox--computed-display box) '(block column)))
|
|
(should (string= (ebox-dsl-test--plain box) "A\nB"))))
|
|
|
|
(ert-deftest ebox-canonical-flex-box-uses-one-box-identity-and-child-list ()
|
|
"A FlexConfig should select the Box algorithm without a Flex runtime node."
|
|
(let* ((left (ebox-text-create :value "A"))
|
|
(right (ebox-text-create :value "B"))
|
|
(layout (ebox-flex-layout-create))
|
|
(box (ebox-box-create :layout layout
|
|
:children (list left right)
|
|
:width '(120)))
|
|
(rendered (ebox-render box)))
|
|
(should
|
|
(equal (ebox-layout-config-props layout)
|
|
'(:flex-direction row :flex-wrap nowrap
|
|
:justify-content flex-start :align-items stretch
|
|
:align-content stretch :row-gap nil :column-gap nil)))
|
|
(should-not (plist-member (ebox-layout-config-props layout) :width))
|
|
(should-not (plist-member (ebox-layout-config-props layout) :height))
|
|
(should (ebox-box-node-p box))
|
|
(should (eq (ebox-layout-config-kind (ebox-box-node-layout box)) 'flex))
|
|
(should (eq (ebox-tree-node-children box)
|
|
(ebox-box-node-children box)))
|
|
(should (= (ebox-dsl-test--tree-count box (lambda (_node) t)) 3))
|
|
(should (= (ebox-dsl-test--tree-count box #'ebox-node-kind) 3))
|
|
(should
|
|
(= (ebox-dsl-test--tree-count
|
|
box (lambda (node) (eq (plist-get node :ebox-type) 'flex)))
|
|
0))
|
|
(should-not (plist-member box :ebox-content-node))
|
|
(should (equal (ebox--computed-display box) '(block flex)))
|
|
(should (string= (string-trim-right
|
|
(substring-no-properties rendered))
|
|
"AB"))
|
|
(should (= (ebox--string-pixel-width rendered) 120))))
|
|
|
|
(ert-deftest ebox-canonical-flex-revalidates-the-typed-config-boundary ()
|
|
"Mutated or extra FlexConfig properties should fail before rendering."
|
|
(let ((invalid-direction (ebox-flex-layout-create))
|
|
(extra-frame-prop (ebox-flex-layout-create))
|
|
(cross-kind-props (ebox-flex-layout-create)))
|
|
(setf (ebox-layout-config-props invalid-direction)
|
|
'(:flex-direction bogus :flex-wrap nowrap
|
|
:justify-content flex-start :align-items stretch
|
|
:align-content stretch :row-gap nil :column-gap nil))
|
|
(setf (ebox-layout-config-props extra-frame-prop)
|
|
(append (ebox-layout-config-props extra-frame-prop)
|
|
'(:width 999)))
|
|
(setf (ebox-layout-config-kind cross-kind-props) 'row)
|
|
(should-error
|
|
(ebox-box-create :layout invalid-direction :children nil)
|
|
:type 'error)
|
|
(should-error
|
|
(ebox-box-create :layout extra-frame-prop :children nil)
|
|
:type 'error)
|
|
(should-error
|
|
(ebox-box-create :layout cross-kind-props :children nil)
|
|
:type 'error)))
|
|
|
|
(ert-deftest ebox-canonical-box-detaches-validated-layout-config ()
|
|
"Caller and accessor mutations should not alter a constructed Box."
|
|
(let* ((source-layout (ebox-flex-layout-create))
|
|
(box (ebox-box-create
|
|
:layout source-layout
|
|
:children (list (ebox-text-create :value "A")
|
|
(ebox-text-create :value "B"))))
|
|
(exposed-layout (ebox-box-node-layout box)))
|
|
(should-not (fboundp 'ebox-layout-config-validator))
|
|
(setf (ebox-layout-config-props source-layout) '(:flex-direction bogus))
|
|
(setf (ebox-layout-config-props exposed-layout) '(:width 999))
|
|
(should (string= (string-trim-right
|
|
(substring-no-properties (ebox-render box)))
|
|
"AB"))
|
|
(should (equal (ebox-layout-config-props (ebox-box-node-layout box))
|
|
ebox--flex-default-config-props))))
|
|
|
|
(ert-deftest ebox-canonical-flex-projects-one-box-frame-constraint ()
|
|
"Typed and legacy Flex should derive the same content size from BoxFrame."
|
|
(dolist (sizing '(border-box content-box))
|
|
(let* ((typed
|
|
(ebox-box-create
|
|
:layout (ebox-flex-layout-create)
|
|
:children (list (ebox-text-create :value "A")
|
|
(ebox-text-create :value "B"))
|
|
:width '(140) :padding '(0 (10)) :box-sizing sizing))
|
|
(legacy
|
|
(ebox-flex
|
|
:width '(140) :padding '(0 (10)) :box-sizing sizing
|
|
(ebox-text-create :value "A")
|
|
(ebox-text-create :value "B")))
|
|
(typed-rendered (ebox-render typed))
|
|
(legacy-rendered (ebox-render legacy)))
|
|
(should
|
|
(string=
|
|
(string-trim-right (substring-no-properties typed-rendered))
|
|
(string-trim-right (substring-no-properties legacy-rendered))))
|
|
(should (= (ebox--string-pixel-width typed-rendered)
|
|
(ebox--string-pixel-width legacy-rendered))))))
|
|
|
|
(ert-deftest ebox-canonical-grid-box-uses-one-box-identity-and-child-list ()
|
|
"A GridConfig should select the Box algorithm without a Grid runtime node."
|
|
(let* ((layout (ebox-grid-layout-create))
|
|
(box (ebox-box-create
|
|
:layout layout
|
|
:children (list (ebox-text-create :value "A")
|
|
(ebox-text-create :value "B"))
|
|
:width '(120)))
|
|
(rendered (ebox-render box)))
|
|
(should (equal (ebox-layout-config-props layout)
|
|
ebox--grid-default-config-props))
|
|
(should-not (plist-member (ebox-layout-config-props layout) :width))
|
|
(should-not (plist-member (ebox-layout-config-props layout) :height))
|
|
(should (ebox-box-node-p box))
|
|
(should (eq (ebox-layout-config-kind (ebox-box-node-layout box)) 'grid))
|
|
(should (eq (ebox-tree-node-children box)
|
|
(ebox-box-node-children box)))
|
|
(should (= (ebox-dsl-test--tree-count box (lambda (_node) t)) 3))
|
|
(should (= (ebox-dsl-test--tree-count box #'ebox-node-kind) 3))
|
|
(should
|
|
(= (ebox-dsl-test--tree-count
|
|
box (lambda (node) (eq (plist-get node :ebox-type) 'grid)))
|
|
0))
|
|
(should-not (plist-member box :ebox-content-node))
|
|
(should (equal (ebox--computed-display box) '(block grid)))
|
|
(should (string=
|
|
(replace-regexp-in-string
|
|
"[[:space:]]" "" (substring-no-properties rendered))
|
|
"AB"))
|
|
(should (equal (mapcar #'ebox--string-pixel-width
|
|
(ebox-string-lines rendered))
|
|
'(120 120 120)))))
|
|
|
|
(ert-deftest ebox-canonical-grid-consumes-direct-child-placement ()
|
|
"Grid placement should stay on child Boxes without grid-item wrappers."
|
|
(let* ((later
|
|
(ebox-box-create :layout (ebox-normal-layout-create)
|
|
:children (list (ebox-text-create :value "A"))
|
|
:grid-row 2))
|
|
(earlier
|
|
(ebox-box-create :layout (ebox-normal-layout-create)
|
|
:children (list (ebox-text-create :value "B"))
|
|
:grid-row 1))
|
|
(box (ebox-box-create :layout (ebox-grid-layout-create)
|
|
:children (list later earlier)))
|
|
(rendered (substring-no-properties (ebox-render box))))
|
|
(should (string=
|
|
(replace-regexp-in-string "[[:space:]]" "" rendered)
|
|
"BA"))
|
|
(should (= (ebox-dsl-test--tree-count box (lambda (_node) t)) 5))
|
|
(should (= (ebox-dsl-test--tree-count box #'ebox-node-kind) 5))))
|
|
|
|
(ert-deftest ebox-canonical-grid-revalidates-the-typed-config-boundary ()
|
|
"GridConfig must reject frame properties and cross-kind property sets."
|
|
(let ((extra-frame-prop (ebox-grid-layout-create))
|
|
(cross-kind-props (ebox-grid-layout-create)))
|
|
(setf (ebox-layout-config-props extra-frame-prop)
|
|
(append (ebox-layout-config-props extra-frame-prop)
|
|
'(:width 999)))
|
|
(setf (ebox-layout-config-kind cross-kind-props) 'column)
|
|
(should-error
|
|
(ebox-box-create :layout extra-frame-prop :children nil)
|
|
:type 'error)
|
|
(should-error
|
|
(ebox-box-create :layout cross-kind-props :children nil)
|
|
:type 'error)))
|
|
|
|
(ert-deftest ebox-canonical-grid-projects-one-box-frame-constraint ()
|
|
"Typed and legacy Grid should derive the same content size from BoxFrame."
|
|
(dolist (sizing '(border-box content-box))
|
|
(let* ((typed
|
|
(ebox-box-create
|
|
:layout (ebox-grid-layout-create)
|
|
:children (list (ebox-text-create :value "A")
|
|
(ebox-text-create :value "B"))
|
|
:width '(140) :padding '(0 (10)) :box-sizing sizing))
|
|
(legacy
|
|
(ebox-grid
|
|
:width '(140) :padding '(0 (10)) :box-sizing sizing
|
|
(ebox-text-create :value "A")
|
|
(ebox-text-create :value "B")))
|
|
(typed-rendered (ebox-render typed))
|
|
(legacy-rendered (ebox-render legacy)))
|
|
(should (string= (substring-no-properties typed-rendered)
|
|
(substring-no-properties legacy-rendered)))
|
|
(should
|
|
(equal (mapcar #'ebox--string-pixel-width
|
|
(ebox-string-lines typed-rendered))
|
|
(mapcar #'ebox--string-pixel-width
|
|
(ebox-string-lines legacy-rendered)))))))
|
|
|
|
(ert-deftest ebox-canonical-flex-consumes-direct-child-participation ()
|
|
"Flex participation should stay on child Boxes without item wrappers."
|
|
(let* ((left-text (ebox-text-create :value "A"))
|
|
(right-text (ebox-text-create :value "B"))
|
|
(left (ebox-box-create :layout (ebox-normal-layout-create)
|
|
:children (list left-text)
|
|
:order 2))
|
|
(right (ebox-box-create :layout (ebox-normal-layout-create)
|
|
:children (list right-text)
|
|
:order 1))
|
|
(box (ebox-box-create :layout (ebox-flex-layout-create)
|
|
:children (list left right)))
|
|
(rendered (substring-no-properties (ebox-render box))))
|
|
(should (string= (string-trim-right rendered) "BA"))
|
|
(should (= (ebox-dsl-test--tree-count box (lambda (_node) t)) 5))
|
|
(should (= (ebox-dsl-test--tree-count box #'ebox-node-kind) 5))
|
|
(should
|
|
(= (ebox-dsl-test--tree-count
|
|
box (lambda (node) (eq (plist-get node :ebox-type) 'flex-item)))
|
|
0))))
|
|
|
|
(ert-deftest ebox-canonical-row-and-column-never-collapse-single-child-boxes ()
|
|
"A one-child Row or Column should preserve its canonical Box boundary."
|
|
(let* ((child (ebox-text-create :value "A"))
|
|
(row (ebox-box-create :layout (ebox-row-layout-create)
|
|
:children (list child)))
|
|
(column (ebox-box-create :layout (ebox-column-layout-create)
|
|
:children (list child))))
|
|
(should (ebox-box-node-p row))
|
|
(should (ebox-box-node-p column))
|
|
(should-not (eq row child))
|
|
(should-not (eq column child))
|
|
(should (equal (ebox-tree-node-children row) (list child)))
|
|
(should (equal (ebox-tree-node-children column) (list child)))
|
|
(should (= (ebox-dsl-test--tree-count row (lambda (_node) t)) 2))
|
|
(should (= (ebox-dsl-test--tree-count column (lambda (_node) t)) 2))
|
|
(should (string= (ebox-dsl-test--plain row) "A"))
|
|
(should (string= (ebox-dsl-test--plain column) "A"))))
|
|
|
|
(ert-deftest ebox-canonical-empty-layout-boxes-have-no-runtime-child ()
|
|
"Empty layout Boxes should not synthesize layout identities."
|
|
(dolist (layout (list (ebox-row-layout-create)
|
|
(ebox-column-layout-create)
|
|
(ebox-flex-layout-create)
|
|
(ebox-grid-layout-create)))
|
|
(let ((box (ebox-box-create :layout layout :children nil)))
|
|
(should (ebox-box-node-p box))
|
|
(should-not (ebox-tree-node-children box))
|
|
(should (= (ebox-dsl-test--tree-count box (lambda (_node) t)) 1))
|
|
(should (= (ebox-dsl-test--tree-count box #'ebox-node-kind) 1))
|
|
(should (string= (ebox-dsl-test--plain box) "")))))
|
|
|
|
(ert-deftest ebox-canonical-box-copy-keeps-one-authoritative-child-list ()
|
|
"A copied Box should expose exactly its copied runtime children."
|
|
(let* ((left (ebox-text-create :value "A"))
|
|
(right (ebox-text-create :value "B"))
|
|
(source (ebox-box-create :layout (ebox-row-layout-create)
|
|
:children (list left right)))
|
|
(copy (ebox-tree-copy-node-structure source))
|
|
(copied-children (ebox-tree-node-children copy)))
|
|
(should (eq copied-children (ebox-box-node-children copy)))
|
|
(should-not (eq copied-children (ebox-box-node-children source)))
|
|
(should-not (eq (car copied-children) left))
|
|
(should-not (eq (cadr copied-children) right))
|
|
(should (= (ebox-dsl-test--tree-count copy (lambda (_node) t)) 3))
|
|
(should (= (ebox-dsl-test--tree-count copy #'ebox-node-kind) 3))))
|
|
|
|
(ert-deftest ebox-canonical-box-rejects-paint-only-properties ()
|
|
"Canonical Box geometry should not duplicate paint-only state."
|
|
(should-error
|
|
(ebox-box-create :layout (ebox-normal-layout-create)
|
|
:bgcolor "red")
|
|
:type 'error))
|
|
|
|
(ert-deftest ebox-canonical-box-requires-unique-reserved-fields ()
|
|
"Typed Box reserved fields should never be missing or duplicated."
|
|
(let ((normal (ebox-normal-layout-create)))
|
|
(should-error (ebox-box-create :children nil) :type 'error)
|
|
(should-error
|
|
(ebox-box-create :layout normal :layout normal :children nil)
|
|
:type 'error)
|
|
(should-error
|
|
(ebox-box-create :layout normal :children nil :children nil)
|
|
:type 'error)
|
|
(should-error
|
|
(ebox-box-create :layout normal :outer nil :children nil)
|
|
:type 'error)))
|
|
|
|
(ert-deftest ebox-build-normalizes-text-author-forms ()
|
|
"The Ebox DSL should normalize explicit and bare Text forms."
|
|
(let ((text (ebox-build '(text "A")))
|
|
(bare (ebox-build "B")))
|
|
(should (ebox-text-node-p text))
|
|
(should (ebox-text-node-p bare))
|
|
(should (string= (ebox-dsl-test--plain text) "A"))
|
|
(should (string= (ebox-dsl-test--plain bare) "B"))))
|
|
|
|
(ert-deftest ebox-build-text-requires-one-string-payload ()
|
|
"The Ebox Text form should reject empty, multiple, and nested payloads."
|
|
(should-error (ebox-build '(text)) :type 'error)
|
|
(should-error (ebox-build '(text "A" "B")) :type 'error)
|
|
(should-error (ebox-build '(text (text "A"))) :type 'error)
|
|
(should-error (ebox-build '(text :color "red" "A")) :type 'error)
|
|
(should-error (ebox-build '(text :wrap-mode garbage "A")) :type 'error))
|
|
|
|
(ert-deftest ebox-dsl-module-preserves-public-build-syntax ()
|
|
"Extracted DSL compiler should preserve existing ebox-build syntax."
|
|
(let ((node (ebox-build
|
|
'(column :padding 1 :bgcolor "linen"
|
|
(row
|
|
(box :content "A" :width (40))
|
|
(box :content "B" :width (40)))))))
|
|
(should (eq (plist-get node :ebox-type) 'box))
|
|
(should (string-match-p "A" (ebox-render node)))
|
|
(should (string-match-p "B" (ebox-render node)))))
|
|
|
|
(ert-deftest ebox-build-creates-box-from-keyword-plist ()
|
|
"A `(box ...)' form should compile to a normal Ebox box node."
|
|
(let ((node (ebox-build '(box :content "Hello" :width (120) :border t))))
|
|
(should (eq (plist-get node :ebox-type) 'box))
|
|
(should (string-match-p "Hello" (ebox-dsl-test--plain node)))))
|
|
|
|
(ert-deftest ebox-build-uses-string-child-as-box-content ()
|
|
"A simple string child should be shorthand for box content."
|
|
(let ((node (ebox-build '(box :width (120) "Hello"))))
|
|
(should (eq (plist-get node :ebox-type) 'box))
|
|
(should (string-match-p "Hello" (ebox-dsl-test--plain node)))))
|
|
|
|
(ert-deftest ebox-build-resolves-viewport-width-from-dynamic-binding ()
|
|
"The DSL should allow box widths to follow the current render viewport."
|
|
(let* ((ebox-viewport-width 320)
|
|
(node (ebox-build
|
|
'(box :content "Viewport"
|
|
:width (viewport)
|
|
:box-sizing border-box
|
|
:border t))))
|
|
(should (= (car (ebox-dsl-test--line-widths node)) 320))))
|
|
|
|
(ert-deftest ebox-build-resolves-viewport-height-from-dynamic-binding ()
|
|
"The DSL should allow box heights to follow the current render viewport."
|
|
(let* ((ebox-viewport-height 3)
|
|
(node (ebox-build
|
|
'(box :content "One\nTwo\nThree\nFour"
|
|
:height (viewport-height)
|
|
:overflow scroll))))
|
|
(should (= (length (ebox-string-lines (ebox-render node))) 3))))
|
|
|
|
(ert-deftest ebox-build-resolves-viewport-height-arithmetic-expression ()
|
|
"The DSL should allow viewport-relative height expressions."
|
|
(let* ((ebox-viewport-height 6)
|
|
(node (ebox-build
|
|
'(box :content "One\nTwo\nThree\nFour"
|
|
:height (- (viewport-height) 2)
|
|
:overflow scroll))))
|
|
(should (= (length (ebox-string-lines (ebox-render node))) 4))))
|
|
|
|
(ert-deftest ebox-build-unquotes-static-list-property-values ()
|
|
"The DSL should accept quoted list values used by `etaf-view' forms."
|
|
(let* ((node (ebox-build
|
|
'(grid :width '(120)
|
|
:grid-template-columns '((40) 1fr)
|
|
:gap '(0 (4))
|
|
(box :content "A")
|
|
(box :content "B"))))
|
|
(props (plist-get node :raw-props))
|
|
(line (car (ebox-string-lines (ebox-render node)))))
|
|
(should (equal (plist-get props :width) '(120)))
|
|
(should (equal (plist-get props :grid-template-columns)
|
|
'((40) 1fr)))
|
|
(should (= (ebox--string-pixel-width line) 120))))
|
|
|
|
(ert-deftest ebox-build-composes-fixed-viewport-shell-around-scrollbox ()
|
|
"A fixed viewport shell can reserve chrome while an inner box scrolls."
|
|
(let* ((ebox-viewport-height 6)
|
|
(node (ebox-build
|
|
'(column :height (viewport-height)
|
|
(box :content "top" :height 1)
|
|
(box :id "root" :height (- (viewport-height) 2)
|
|
:overflow scroll
|
|
(column
|
|
(box :content "A" :height 1)
|
|
(box :content "B" :height 1)
|
|
(box :content "C" :height 1)
|
|
(box :content "D" :height 1)
|
|
(box :content "E" :height 1)))
|
|
(box :content "bottom" :height 1)))))
|
|
(should (= (length (ebox-string-lines (ebox-render node))) 6))))
|
|
|
|
(ert-deftest ebox-build-composes-row-and-column-layouts ()
|
|
"Row and column forms should compose existing lazy layout nodes."
|
|
(let* ((node (ebox-build
|
|
'(column
|
|
(box :content "Header" :width (180))
|
|
(row
|
|
(box :content "Left" :width (80))
|
|
(spacer :width (20))
|
|
(box :content "Right" :width (80))))))
|
|
(plain (ebox-dsl-test--plain node)))
|
|
(should (eq (plist-get node :ebox-type) 'stack))
|
|
(should (string-match-p "Header" plain))
|
|
(should (string-match-p "Left.*Right" plain))
|
|
(should (= (length (ebox-region-ids node)) 4))))
|
|
|
|
(ert-deftest ebox-build-wraps-child-layout-with-box-properties ()
|
|
"A node with child layout may still apply box properties to the wrapper."
|
|
(let* ((node (ebox-build
|
|
'(box :width (240) :box-sizing border-box
|
|
:padding (1 20) :border t :bgcolor "#222222"
|
|
(row
|
|
(box :content "A" :width (80))
|
|
(box :content "B" :width (80))))))
|
|
(plain (ebox-dsl-test--plain node)))
|
|
(should (eq (plist-get node :ebox-type) 'box))
|
|
(should (equal (ebox-get node :bgcolor) "#222222"))
|
|
(should (string-match-p "A.*B" plain))
|
|
(dolist (width (ebox-dsl-test--line-widths node))
|
|
(should (= width 240)))))
|
|
|
|
(ert-deftest ebox-build-box-wrapper-preserves-preformatted-child-layout ()
|
|
"A box wrapping child layout should not rewrap rendered children."
|
|
(cl-letf (((symbol-function 'require)
|
|
(lambda (feature &optional _filename _noerror)
|
|
(when (eq feature 'ekp) t)))
|
|
((symbol-function 'ekp-pixel-justify)
|
|
(lambda (text _width)
|
|
(if (string-match-p "\n" text)
|
|
"REWRAPPED"
|
|
text))))
|
|
(let* ((node (ebox-build
|
|
'(box :width (220) :box-sizing border-box
|
|
(row
|
|
(box :content "Left" :width (80) :height 2)
|
|
(box :content "Right" :width (80) :height 2)))))
|
|
(plain (ebox-dsl-test--plain node)))
|
|
(should-not (string-match-p "REWRAPPED" plain))
|
|
(should (string-match-p "Left.*Right" plain))
|
|
(dolist (width (ebox-dsl-test--line-widths node))
|
|
(should (= width 220))))))
|
|
|
|
(ert-deftest ebox-build-row-wrapper-preserves-preformatted-layout ()
|
|
"Row box properties should not rewrap the rendered child layout."
|
|
(cl-letf (((symbol-function 'require)
|
|
(lambda (feature &optional _filename _noerror)
|
|
(when (eq feature 'ekp) t)))
|
|
((symbol-function 'ekp-pixel-justify)
|
|
(lambda (text _width)
|
|
(if (string-match-p "\n" text)
|
|
"REWRAPPED"
|
|
text))))
|
|
(let* ((node (ebox-build
|
|
'(row :width (220) :box-sizing border-box
|
|
(box :content "Left" :width (80) :height 2)
|
|
(box :content "Right" :width (80) :height 2))))
|
|
(plain (ebox-dsl-test--plain node)))
|
|
(should-not (string-match-p "REWRAPPED" plain))
|
|
(should (string-match-p "Left.*Right" plain))
|
|
(dolist (width (ebox-dsl-test--line-widths node))
|
|
(should (= width 220))))))
|
|
|
|
(ert-deftest ebox-build-column-wrapper-preserves-preformatted-layout ()
|
|
"Column box properties should not rewrap the rendered child layout."
|
|
(cl-letf (((symbol-function 'require)
|
|
(lambda (feature &optional _filename _noerror)
|
|
(when (eq feature 'ekp) t)))
|
|
((symbol-function 'ekp-pixel-justify)
|
|
(lambda (text _width)
|
|
(if (string-match-p "\n" text)
|
|
"REWRAPPED"
|
|
text))))
|
|
(let* ((node (ebox-build
|
|
'(column :width (180) :box-sizing border-box
|
|
(box :content "Top" :width (120))
|
|
(box :content "Bottom" :width (120)))))
|
|
(plain (ebox-dsl-test--plain node)))
|
|
(should-not (string-match-p "REWRAPPED" plain))
|
|
(should (string-match-p "Top" plain))
|
|
(should (string-match-p "Bottom" plain))
|
|
(dolist (width (ebox-dsl-test--line-widths node))
|
|
(should (= width 180))))))
|
|
|
|
(ert-deftest ebox-build-padded-column-wrapper-stays-inside-viewport ()
|
|
"A padded layout wrapper should not let preformatted children overflow."
|
|
(let* ((ebox-viewport-width 320)
|
|
(node (ebox-build
|
|
'(column :padding 1
|
|
(box :content "Child width follows the viewport"
|
|
:wrap-mode word))))
|
|
(widths (ebox-dsl-test--display-line-widths node)))
|
|
(should (cl-every (lambda (width)
|
|
(<= width ebox-viewport-width))
|
|
widths))))
|
|
|
|
(ert-deftest ebox-build-wrapper-explicit-width-bounds-child-layout-viewport ()
|
|
"A wrapper's explicit width should be the containing block for child layouts."
|
|
(let* ((ebox-viewport-width 360)
|
|
(node (ebox-build
|
|
'(column :width (180)
|
|
(box :content "alpha beta gamma delta epsilon zeta eta theta"
|
|
:wrap-mode word))))
|
|
(widths (ebox-dsl-test--line-widths node)))
|
|
(dolist (width widths)
|
|
(should (<= width 180)))))
|
|
|
|
(ert-deftest ebox-build-column-contains-auto-row-children ()
|
|
"A column wrapper must not let an auto-width row child duplicate its viewport."
|
|
(let* ((node (ebox-build
|
|
'(column :width (120)
|
|
(row
|
|
(box :content "Left" :border t)
|
|
(box :content "Right" :border t))
|
|
(box :content "Body" :border t))))
|
|
(widths (ebox-dsl-test--line-widths node)))
|
|
(should (cl-every (lambda (width) (<= width 120)) widths))))
|
|
|
|
(ert-deftest ebox-row-keeps-explicit-viewport-widths-intrinsic-context ()
|
|
"Row intrinsic sizing must not change explicit viewport-sized children."
|
|
(let* ((ebox-viewport-width 120)
|
|
(node (ebox-row
|
|
(ebox-create :content "Left" :width 'viewport)
|
|
(ebox-create :content "Right" :width 'stretch)))
|
|
(widths (ebox-dsl-test--line-widths node)))
|
|
(should (equal widths '(240)))))
|
|
|
|
(ert-deftest ebox-row-keeps-composite-column-intrinsic-width ()
|
|
"A composite column child must not consume the row viewport as auto width."
|
|
(let* ((ebox-viewport-width 120)
|
|
(node (ebox-row
|
|
(ebox-column
|
|
(ebox-create :content "Top")
|
|
(ebox-create :content "Bottom"))
|
|
(ebox-create :content "Tail")))
|
|
(widths (ebox-dsl-test--line-widths node)))
|
|
(should (cl-every (lambda (width) (<= width 120)) widths))))
|
|
|
|
(ert-deftest ebox-build-lazy-column-contains-auto-row-children ()
|
|
"Lazy column windows must apply the same intrinsic row sizing rule."
|
|
(let* ((ebox-viewport-width 120)
|
|
(ebox-viewport-height 4)
|
|
(node (ebox-build
|
|
'(box :width (viewport)
|
|
:height (viewport-height)
|
|
:overflow scroll
|
|
(column
|
|
(row
|
|
(box :content "Left" :border t)
|
|
(box :content "Right" :border t))
|
|
(box :content "Body" :border t)))))
|
|
(widths (ebox-dsl-test--line-widths node)))
|
|
(should (cl-every (lambda (width) (<= width 120)) widths))))
|
|
|
|
(ert-deftest ebox-build-wrapper-viewport-width-bounds-child-layout-viewport ()
|
|
"A wrapper's `(viewport)' width should pass its content box to child layouts."
|
|
(let ((cases
|
|
'((box :width (viewport)
|
|
:padding-inline (20)
|
|
:border-left (10)
|
|
:border-right (10)
|
|
(box :content "alpha beta gamma delta epsilon zeta eta theta"
|
|
:wrap-mode word))
|
|
(column :width (viewport)
|
|
:padding-inline (20)
|
|
:border-left (10)
|
|
:border-right (10)
|
|
(box :content "alpha beta gamma delta epsilon zeta eta theta"
|
|
:wrap-mode word))
|
|
(row :width (viewport)
|
|
:padding-inline (20)
|
|
:border-left (10)
|
|
:border-right (10)
|
|
(box :content "alpha beta gamma delta epsilon zeta eta theta"
|
|
:wrap-mode word)
|
|
(spacer :width (0) :height 1)))))
|
|
(dolist (dsl cases)
|
|
(let* ((ebox-viewport-width 240)
|
|
(node (ebox-build dsl))
|
|
(widths (ebox-dsl-test--line-widths node)))
|
|
(dolist (width widths)
|
|
(should (<= width ebox-viewport-width)))))))
|
|
|
|
(ert-deftest ebox-build-lazy-scroll-prefix-bounds-wrapper-child-viewport ()
|
|
"Lazy scroll prefix rendering should preserve wrapper content viewport."
|
|
(let* ((ebox-viewport-width 320)
|
|
(ebox-viewport-height 5)
|
|
(node (ebox-build
|
|
'(box :id "root"
|
|
:width (viewport)
|
|
:height (- (viewport-height) 1)
|
|
:box-sizing border-box
|
|
:overflow scroll
|
|
(column :width (viewport)
|
|
:box-sizing border-box
|
|
:padding (1 (18))
|
|
(box :content "One"
|
|
:width (viewport)
|
|
:box-sizing border-box
|
|
:border t)
|
|
(box :content "Two"
|
|
:width (viewport)
|
|
:box-sizing border-box
|
|
:border t)
|
|
(box :content "Three"
|
|
:width (viewport)
|
|
:box-sizing border-box
|
|
:border t)
|
|
(box :content "Four"
|
|
:width (viewport)
|
|
:box-sizing border-box
|
|
:border t)))))
|
|
(widths (ebox-dsl-test--line-widths node)))
|
|
(dolist (width widths)
|
|
(should (<= width ebox-viewport-width)))))
|
|
|
|
(ert-deftest ebox-build-tags-map-to-css-like-display ()
|
|
"DSL container tags should expose CSS-like outer/inner display values."
|
|
(let ((box-node (ebox-build '(box :content "A")))
|
|
(row-node (ebox-build '(row (box :content "A") (box :content "B"))))
|
|
(column-node (ebox-build
|
|
'(column (box :content "A") (box :content "B"))))
|
|
(flex-node (ebox-build '(flex (box :content "A") (box :content "B")))))
|
|
(should (equal (ebox--computed-display box-node) '(block flow)))
|
|
(should (equal (ebox--computed-display row-node) '(block row)))
|
|
(should (equal (ebox--computed-display column-node) '(block column)))
|
|
(should (equal (ebox--computed-display flex-node) '(block flex)))))
|
|
|
|
(ert-deftest ebox-build-box-uses-orthogonal-outer-and-layout-axes ()
|
|
"Canonical Box DSL should select outer participation and child layout."
|
|
(let ((normal (ebox-build '(box :outer inline :layout normal "A")))
|
|
(row (ebox-build
|
|
'(box :outer inline :layout row
|
|
(box "A")
|
|
(box "B"))))
|
|
(column (ebox-build
|
|
'(box :outer block :layout column
|
|
(box "A")
|
|
(box "B")))))
|
|
(should (equal (ebox--computed-display normal) '(inline flow)))
|
|
(should (equal (ebox--computed-display row) '(inline row)))
|
|
(should (equal (ebox--computed-display column) '(block column)))
|
|
(should (string= (ebox-dsl-test--plain row) "AB"))
|
|
(should (string= (ebox-dsl-test--plain column) "A\nB"))))
|
|
|
|
(ert-deftest ebox-build-box-rejects-invalid-axis-values ()
|
|
"Canonical Box DSL should reject unknown outer and layout values."
|
|
(should-error (ebox-build '(box :outer floating "A")) :type 'error)
|
|
(should-error (ebox-build '(box :layout masonry "A")) :type 'error))
|
|
|
|
(ert-deftest ebox-build-box-layout-consumes-direct-flex-item-properties ()
|
|
"Canonical Flex Box should consume participation props from child Boxes."
|
|
(let* ((node (ebox-build
|
|
'(box :layout flex :width (300)
|
|
(box :flex-grow 1 :flex-basis (80) "A")
|
|
(box :flex-grow 2 :flex-basis (80) "B"))))
|
|
(children (plist-get node :children)))
|
|
(should (equal (ebox--computed-display node) '(block flex)))
|
|
(should (= (plist-get (car children) :flex-grow) 1))
|
|
(should (= (plist-get (cadr children) :flex-grow) 2))
|
|
(should (string-match-p "A" (ebox-dsl-test--plain node)))
|
|
(should (string-match-p "B" (ebox-dsl-test--plain node)))))
|
|
|
|
(ert-deftest ebox-build-box-layout-consumes-direct-grid-item-properties ()
|
|
"Canonical Grid Box should consume placement props from child Boxes."
|
|
(let* ((node (ebox-build
|
|
'(box :layout grid :grid-template-columns ((20) (20))
|
|
(box :grid-column (1 :span 2) "Header"))))
|
|
(child (car (plist-get node :children))))
|
|
(should (equal (ebox--computed-display node) '(block grid)))
|
|
(should (equal (plist-get child :grid-column) '(1 :span 2)))
|
|
(should (string-match-p "Header" (ebox-dsl-test--plain node)))))
|
|
|
|
(ert-deftest ebox-build-wrapper-child-layout-rerenders-on-runtime-viewport-change ()
|
|
"A DSL wrapper box should rerender child layout content on viewport resize."
|
|
(let ((node (let ((ebox-viewport-width 96))
|
|
(ebox-build
|
|
'(column :padding 1 :bgcolor "linen"
|
|
(box :content "This child text should wrap again when the preview becomes narrow."
|
|
:wrap-mode word)))))
|
|
buffer)
|
|
(unwind-protect
|
|
(progn
|
|
(setq buffer
|
|
(let ((ebox-viewport-width 96))
|
|
(ebox-render-to-buffer
|
|
(generate-new-buffer-name " *ebox-dsl-test*")
|
|
node)))
|
|
(let ((wide-text (with-current-buffer buffer
|
|
(buffer-substring-no-properties
|
|
(point-min) (point-max))))
|
|
(wide-height (with-current-buffer buffer
|
|
(count-lines (point-min) (point-max)))))
|
|
(ebox-rerender-buffer-with-context buffer 36)
|
|
(let ((narrow-text (with-current-buffer buffer
|
|
(buffer-substring-no-properties
|
|
(point-min) (point-max))))
|
|
(narrow-height (with-current-buffer buffer
|
|
(count-lines (point-min) (point-max)))))
|
|
(should-not (string= wide-text narrow-text))
|
|
(should (> narrow-height wide-height)))))
|
|
(when (and buffer (buffer-live-p buffer))
|
|
(kill-buffer buffer)))))
|
|
|
|
(ert-deftest ebox-build-item-is-flex-participation-sugar ()
|
|
"DSL item should attach flex participation to its child semantics."
|
|
(let* ((node (ebox-build
|
|
'(flex :width (300)
|
|
(item :flex 1 (box :content "A"))
|
|
(box :content "B" :flex 1))))
|
|
(rendered (let ((ebox-viewport-width 300))
|
|
(ebox-render node))))
|
|
(should (string-match-p "A" (substring-no-properties rendered)))
|
|
(should (string-match-p "B" (substring-no-properties rendered)))))
|
|
|
|
(ert-deftest ebox-build-keeps-pre-render-region-ids-available ()
|
|
"The DSL should not break the pre-render region-id workflow."
|
|
(let* ((node (ebox-build
|
|
'(row
|
|
(box :content "One" :width (80))
|
|
(box :content "Two" :width (80)))))
|
|
(ids (ebox-region-ids node)))
|
|
(should (= (length ids) 2))
|
|
(should (cl-every #'identity ids))))
|
|
|
|
(ert-deftest ebox-build-wrapper-region-ids-include-child-layout ()
|
|
"A wrapper box should keep both its own region id and child layout ids."
|
|
(let* ((node (ebox-build
|
|
'(column :padding 1
|
|
(box :content "One")
|
|
(box :content "Two"))))
|
|
(ids (ebox-region-ids node)))
|
|
(should (= (length ids) 3))
|
|
(should (cl-every #'identity ids))))
|
|
|
|
(ert-deftest ebox-build-rejects-invalid-dsl ()
|
|
"Bad forms should fail loudly instead of producing surprising nodes."
|
|
(should-error (ebox-build '(unknown :content "Nope")) :type 'error)
|
|
(should-error (ebox-build '(box :content)) :type 'error)
|
|
(should-error (ebox-build '(spacer :width (20) (box :content "bad"))) :type 'error))
|
|
|
|
(provide 'ebox-dsl-tests)
|
|
;;; ebox-dsl-tests.el ends here
|