;;; 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-inline-runs-and-blocks () "Normal should retain typed children and honor inline/block boundaries." (let* ((text (ebox-text-create :value "A" :source-handle 'source-text)) (second-text (ebox-text-create :value "B")) (inline (ebox-box-create :layout (ebox-normal-layout-create) :outer 'inline :width '(100) :children (list (ebox-text-create :value "A B")))) (block (ebox-box-create :layout (ebox-normal-layout-create) :children (list (ebox-text-create :value "C")))) (tail (ebox-text-create :value "D")) (layout (ebox-normal-layout-create)) (box (ebox-box-create :layout layout :children (list text second-text inline block tail) :source-handle 'source-box :width '(50) :overflow 'visible)) (rendered (ebox-render box)) (rendered-lines (ebox-string-lines rendered)) (lines (mapcar (lambda (line) (string-trim-right (substring-no-properties line))) rendered-lines))) (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 second-text inline block tail))) (should (equal (ebox--computed-display text) '(inline flow))) (should (equal lines '("AB" "A B" "C" "D"))) (should (>= (ebox--string-pixel-width (nth 1 rendered-lines)) 100)) (let ((ebox-style-stylesheet (ecss-stylesheet-create))) (plist-put text :class "fixed-inline-text") (ebox-style-add-rule ".fixed-inline-text" (list :outer 'block)) (should-error (ebox-render box) :type 'error)) (cl-labels ((normal-lines (value &optional width) (mapcar #'substring-no-properties (ebox-string-lines (ebox-render (apply #'ebox-box-create :layout (ebox-normal-layout-create) :children (list (ebox-text-create :value value)) (when width (list :width (list width) :overflow 'visible)))))))) (dolist (case '(("A\n\nB" ("A" "" "B")) ("\nA" ("" "A")) ("A\n" ("A" "")) ("\n" ("" "")))) (should (equal (mapcar #'string-trim-right (normal-lines (car case))) (cadr case)))) (should (equal (normal-lines " A") '(" A"))) (should (equal (normal-lines " ") '(" "))) (should (equal (mapcar #'string-trim-right (normal-lines "abcdef x" (* 5 (ebox--string-pixel-width "a")))) '("abcde" "f x"))) (should (= (length (normal-lines (make-string 10 ?\s) (* 5 (ebox--string-pixel-width " ")))) 2)) (should (= (length (normal-lines "word" 0)) 1)) (should (= (length (normal-lines " " 0)) 1)) (let* ((inline-zero (ebox-box-create :layout (ebox-normal-layout-create) :outer 'inline :width '(10) :children (list (ebox-text-create :value "I")))) (zero-parent (ebox-box-create :layout (ebox-normal-layout-create) :width '(0) :overflow 'visible :children (list inline-zero)))) (should (= (ebox-string-height (ebox-render zero-parent)) 1)))))) (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\nB\nB" :source-handle 'right)) (box (ebox-box-create :layout (ebox-row-layout-create :item-gap 5 :cross-align 'center) :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-layout-config-props (ebox-box-node-layout box)) '(:item-gap 5 :cross-align center))) (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))) (let* ((rendered (ebox-render box)) (lines (ebox-string-lines rendered)) (middle (nth 1 lines))) (should-not (string-match-p "A" (substring-no-properties (car lines)))) (should (string-match-p "A.*B" (substring-no-properties middle))) (should (= (ebox--string-pixel-width middle) (+ (ebox--string-pixel-width "A") 5 (ebox--string-pixel-width "B"))))))) (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 "BBB")) (box (ebox-box-create :layout (ebox-column-layout-create :item-gap 1 :cross-align 'end) :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-layout-config-props (ebox-box-node-layout box)) '(:item-gap 1 :cross-align end))) (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 (= (ebox-string-height (ebox-render box)) 3)) (let ((lines (ebox-string-lines (ebox-render box)))) (should (string-prefix-p " " (substring-no-properties (car lines)))) (should (= (ebox--string-pixel-width (car lines)) (ebox--string-pixel-width (nth 2 lines))))))) (ert-deftest ebox-canonical-simple-axis-config-validates-closed-schema () "Row/Column configs should distinguish defaults from invalid explicit values." (dolist (constructor '(ebox-row-layout-create ebox-column-layout-create)) (should (equal (ebox-layout-config-props (funcall constructor)) '(:item-gap 0 :cross-align stretch))) (dolist (plist '((:item-gap nil) (:item-gap -1) (:cross-align nil) (:cross-align bogus) (:gap 1) (:item-gap 1 :item-gap 2))) (should-error (apply constructor plist) :type 'error)))) (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 0 :column-gap 0))) (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 0 :column-gap 0)) (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-flex-layout-create-normalizes-the-complete-config-schema () "The typed Flex constructor should emit one canonical closed property set." (let ((layout (ebox-flex-layout-create :flex-direction 'column-reverse :flex-wrap 'wrap :justify-content 'space-between :align-items 'center :align-content 'end :row-gap '(2) :column-gap '(8)))) (should (equal (ebox-layout-config-props layout) '(:flex-direction column-reverse :flex-wrap wrap :justify-content space-between :align-items center :align-content end :row-gap 2 :column-gap 8))) (should (ebox-flex-layout-config-props-p (ebox-layout-config-props layout))) (should (ebox-box-node-p (ebox-box-create :layout layout :children nil))))) (ert-deftest ebox-flex-layout-create-validates-values-and-rejects-author-sugar () "FlexConfig should accept its value domain and reject sugar or foreign props." (dolist (direction ebox--flex-direction-values) (should (ebox-flex-layout-create :flex-direction direction))) (dolist (wrap ebox--flex-wrap-values) (should (ebox-flex-layout-create :flex-wrap wrap))) (dolist (justify ebox--flex-justify-content-values) (should (ebox-flex-layout-create :justify-content justify))) (dolist (align ebox--flex-align-items-values) (should (ebox-flex-layout-create :align-items align))) (dolist (align ebox--flex-align-content-values) (should (ebox-flex-layout-create :align-content align))) (dolist (plist '((:flex-direction bogus) (:flex-wrap reverse) (:justify-content bogus) (:align-items bogus) (:align-content bogus) (:row-gap -1) (:column-gap (-1)) (:flex-flow (row wrap)) (:gap 1) (:width 100) (:flex-direction row :flex-direction column))) (should-error (apply #'ebox-flex-layout-create plist) :type 'error))) (ert-deftest ebox-flex-config-validation-does-not-reparse-canonical-pixels () "Boundary validation should preserve physical pixels under non-unit fonts." (cl-letf (((symbol-function 'ebox--space-pixel-width) (lambda () 8))) (let* ((layout (ebox-flex-layout-create :column-gap '(8))) (box (ebox-box-create :layout layout :children nil))) (should (= (plist-get (ebox-layout-config-props layout) :column-gap) 8)) (should (= (plist-get (ebox-layout-config-props (ebox-box-node-layout box)) :column-gap) 8))))) (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-participation-validates-the-final-parent-context () "Participation should be legal only under its matching direct parent layout." (cl-labels ((child (&rest props) (apply #'ebox-box-create :layout (ebox-normal-layout-create) :children (list (ebox-text-create :value "A")) props)) (parent (layout child) (ebox-box-create :layout layout :children (list child)))) (should (stringp (ebox-render (parent (ebox-flex-layout-create) (child :flex-grow 1))))) (should (stringp (ebox-render (parent (ebox-grid-layout-create) (child :grid-row 1))))) (should-error (ebox-render (child :flex-grow 1)) :type 'error) (should-error (ebox-render (parent (ebox-normal-layout-create) (child :flex-grow 1))) :type 'error) (should-error (ebox-render (parent (ebox-flex-layout-create) (child :grid-row 1))) :type 'error) (should-error (ebox-render (parent (ebox-grid-layout-create) (child :flex-grow 1))) :type 'error) (let ((text (ebox-text-create :value "A"))) (plist-put text :flex-grow 1) (should-error (ebox-render (parent (ebox-flex-layout-create) text)) :type 'error)) ;; The final candidate is authoritative after ECSS projection too. (let ((ebox-style-stylesheet (ecss-stylesheet-create))) (ebox-style-add-rule "box" (list :flex-grow 1)) (should-error (ebox-render (parent (ebox-normal-layout-create) (child))) :type 'error)) ;; Detached Flex geometry retains its final parent during computed style. (let ((ebox-style-stylesheet (ecss-stylesheet-create)) (detached (child))) (plist-put detached :class "detached-flex-item") (ebox-style-add-rule ".detached-flex-item" (list :flex-grow 1)) (let ((ebox--render-root-parent-kind 'flex)) (should (stringp (ebox-render detached)))) ;; The dynamic context is exact and cannot authorize a later root. (should-error (ebox-render detached) :type 'error)) (let ((ebox-style-stylesheet (ecss-stylesheet-create)) (detached (child))) (plist-put detached :class "detached-flex-item") (ebox-style-add-rule ".detached-flex-item" (list :grid-row 1)) (let ((ebox--render-root-parent-kind 'flex)) (should-error (ebox-render detached) :type 'error))) ;; A real legacy adapter exempts only its two exact duplicate nodes. (let* ((illegal-grandchild (child :flex-grow 1)) (nested (parent (ebox-normal-layout-create) illegal-grandchild)) (adapter (ebox-flex-item nested :flex-grow 1 :padding 1))) ;; The obsolete mutable depth field must have no authority. (plist-put adapter :ebox-flex-item-adapter-depth 99) (should-error (ebox-render (ebox-flex :width '(100) adapter)) :type 'error)) ;; Provenance binds the exact constructor-time source identities. (let* ((adapter (ebox-flex-item (ebox-create :content "original"))) (replacement (ebox-create :content "replacement"))) (plist-put replacement :flex-grow 1) (plist-put adapter :node replacement) (should-error (ebox-render (ebox-column adapter)) :type 'error)) (let* ((adapter (ebox-flex-item (ebox-create :content "original"))) (source (plist-get adapter :node)) (replacement (ebox-create :content "replacement"))) (plist-put replacement :flex-grow 1) (setq adapter (ebox-tree-copy-with-direct-child-replacements adapter (list (cons source replacement)))) (should-error (ebox-render (ebox-column adapter)) :type 'error)) (let* ((adapter (ebox-flex-item (ebox-create :content "original") :padding 1)) (wrapper (plist-get adapter :node)) (replacement (ebox-create :content "replacement"))) (plist-put replacement :flex-grow 1) (plist-put wrapper :ebox-content-node replacement) (should-error (ebox-render (ebox-column adapter)) :type 'error)) ;; Internal layout and parent participation are orthogonal facts. (should (stringp (ebox-render (parent (ebox-grid-layout-create) (ebox-box-create :layout (ebox-flex-layout-create) :children (list (ebox-text-create :value "A")) :grid-row 1))))))) (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-grid-layout-create-normalizes-the-complete-config-schema () "The typed Grid constructor should emit one canonical closed property set." (let* ((layout (ebox-grid-layout-create :grid-template-columns '((20) 1fr) :grid-template-rows '(1 auto) :grid-auto-columns '((30)) :grid-auto-rows '(2) :grid-auto-flow 'column :row-gap '(2) :column-gap '(8) :justify-items 'center :align-items 'end :justify-content 'space-between :align-content 'center)) (props (ebox-layout-config-props layout))) (should (equal (plist-get props :grid-template-columns) '((:kind fixed :size 20) (:kind fr :factor 1)))) (should (equal (plist-get props :grid-template-rows) '((:kind fixed :size 1) (:kind auto :factor 0)))) (should (equal (plist-get props :grid-auto-columns) '(:kind fixed :size 30))) (should (equal (plist-get props :grid-auto-rows) '(:kind fixed :size 2))) (should (= (plist-get props :row-gap) 2)) (should (= (plist-get props :column-gap) 8)) (should (ebox-grid-layout-config-props-p props)) (should (ebox-box-node-p (ebox-box-create :layout layout :children nil))))) (ert-deftest ebox-grid-layout-create-validates-values-and-rejects-author-sugar () "GridConfig should accept its value domain and reject sugar or foreign props." (dolist (flow '(row column)) (should (ebox-grid-layout-create :grid-auto-flow flow))) (dolist (value ebox--grid-justify-items-values) (should (ebox-grid-layout-create :justify-items value))) (dolist (value ebox--grid-align-items-values) (should (ebox-grid-layout-create :align-items value))) (dolist (value ebox--grid-content-alignment-values) (should (ebox-grid-layout-create :justify-content value)) (should (ebox-grid-layout-create :align-content value))) (dolist (plist '((:grid-auto-flow diagonal) (:grid-template-columns (bogus)) (:grid-template-rows (bogus)) (:grid-template-columns (0fr)) (:grid-template-columns (0.0fr)) (:grid-template-columns ((fr 0))) (:grid-template-columns ((minmax (fr 1) auto))) (:grid-template-columns ((minmax (minmax (10) (20)) auto))) (:grid-auto-columns ((10) (20))) (:row-gap -1) (:column-gap (-1)) (:gap 1) (:grid-row-gap 1) (:width 100) (:align-items bogus) (:grid-auto-flow row :grid-auto-flow column))) (should-error (apply #'ebox-grid-layout-create plist) :type 'error))) (ert-deftest ebox-grid-config-validation-does-not-reparse-canonical-pixels () "Grid boundary validation should preserve physical pixels under GUI fonts." (cl-letf (((symbol-function 'ebox--space-pixel-width) (lambda () 8))) (let* ((layout (ebox-grid-layout-create :grid-template-columns '((20)) :column-gap '(8))) (box (ebox-box-create :layout layout :children nil)) (props (ebox-layout-config-props (ebox-box-node-layout box)))) (should (= (plist-get props :column-gap) 8)) (should (= (plist-get (car (plist-get props :grid-template-columns)) :size) 20))))) (ert-deftest ebox-grid-config-boundary-rejects-forged-minmax-roles () "Trusted Grid validation should enforce minmax roles after mutation." (let ((fr-minimum (ebox-grid-layout-create)) (nested-minimum (ebox-grid-layout-create))) (setf (ebox-layout-config-props fr-minimum) (plist-put (ebox-layout-config-props fr-minimum) :grid-template-columns '((:kind minmax :min (:kind fr :factor 1) :max (:kind auto :factor 0))))) (setf (ebox-layout-config-props nested-minimum) (plist-put (ebox-layout-config-props nested-minimum) :grid-template-columns '((:kind minmax :min (:kind minmax :min (:kind fixed :size 1) :max (:kind fixed :size 2)) :max (:kind auto :factor 0))))) (should-error (ebox-box-create :layout fr-minimum :children nil) :type 'error) (should-error (ebox-box-create :layout nested-minimum :children nil) :type 'error))) (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