;;; etaf-dynamic-components-tests.el --- Dynamic View ownership -*- lexical-binding: t; -*- ;;; Commentary: ;; Ordinary structural expressions use the same retained Components as DSL ;; calls. These tests exercise identity, local publication, and retirement. ;;; Code: (require 'ert) (require 'etaf) (defvar etaf-dynamic-test-setups nil) (defvar etaf-dynamic-test-disposals nil) (defvar etaf-dynamic-test-hooks nil) (defvar etaf-dynamic-test-actions nil) (etaf-define-component etaf-dynamic-test-counter (&key label) :setup (let ((count (etaf-ref 0)) (initial label)) (push (cons initial count) etaf-dynamic-test-setups) (etaf-on-scope-dispose (lambda () (push initial etaf-dynamic-test-disposals))) (etaf-on-mounted (lambda () (push (cons initial 'mounted) etaf-dynamic-test-hooks))) (etaf-on-updated (lambda () (push (cons initial 'updated) etaf-dynamic-test-hooks))) (etaf-on-unmounted (lambda () (push (cons initial 'unmounted) etaf-dynamic-test-hooks))) count) :render (let ((count (etaf-state)) (snapshot label)) (etaf-node 'box (list :on-press (lambda () (push snapshot etaf-dynamic-test-actions) (setf (etaf-value count) (1+ (etaf-value count))))) (list (format "%s:%d" label (etaf-value count)))))) (etaf-define-component etaf-dynamic-test-owner (&key source) :view (column (expr (etaf-value source)))) (etaf-define-component etaf-dynamic-test-slot-inner () :view (column (slot (text "FALLBACK")))) (etaf-define-component etaf-dynamic-test-slot-outer (&key label) :view (etaf-dynamic-test-slot-inner (box :ref 'etaf-dynamic-slot-snapshot :on-press (let ((snapshot (list label (not (null (etaf-current-slots)))))) (lambda () (push snapshot etaf-dynamic-test-actions))) (text (expr (or label "EMPTY"))) (text (expr (if (etaf-current-slot 'default) "HAS-SLOT" "NO-SLOT")))))) (etaf-define-component etaf-dynamic-test-context-provider (&key cell) :setup (progn (etaf-provide 'etaf-dynamic-slot-context "INNER") nil) :view (column (slot) (expr (funcall cell)))) (etaf-define-component etaf-dynamic-test-context-reader () :setup (etaf-inject 'etaf-dynamic-slot-context "ROOT") :view (text (expr (etaf-state)))) (etaf-define-component etaf-dynamic-test-context-author (&key label cell) :view (etaf-dynamic-test-context-provider :cell cell (text (expr (concat (etaf-inject 'etaf-dynamic-slot-context "ROOT") ":" (etaf-value label)))) (etaf-dynamic-test-context-reader))) (etaf-define-component etaf-dynamic-test-pure (&key label) :view (text (expr label))) (etaf-define-component etaf-dynamic-test-pair () :view (fragment (etaf-dynamic-test-counter :label "A") (etaf-dynamic-test-counter :label "B"))) (etaf-define-component etaf-dynamic-test-range-pair (&key left right) :view (fragment (etaf-dynamic-test-owner :source left) (etaf-dynamic-test-owner :source right))) (etaf-define-component etaf-dynamic-test-wrapper (&key label) :view (column (etaf-dynamic-test-counter :label label))) (defun etaf-dynamic-test--counter (label &optional key) "Return one counter description for LABEL with optional KEY." (etaf-node 'etaf-dynamic-test-counter (append (list :label label) (and key (list :key key))) nil)) (etaf-define-component etaf-dynamic-test-programmatic-keyed-item (&key entry) :render (etaf-node 'column (list :key (car entry)) (list (etaf-node 'row nil (list (etaf-dynamic-test--counter (cdr entry))))))) (defun etaf-dynamic-test--text (buffer) "Return the visible text in BUFFER." (with-current-buffer buffer (buffer-substring-no-properties (point-min) (point-max)))) (defun etaf-dynamic-test--close (buffer) "Unmount and kill test BUFFER if it exists." (when-let* ((runtime (etaf-runtime-for-buffer buffer))) (etaf-unmount runtime)) (when (buffer-live-p buffer) (kill-buffer buffer))) (defmacro etaf-dynamic-test--with-runtime (view &rest body) "Mount VIEW, evaluate BODY with buffer/runtime bindings, then dispose." (declare (indent 1) (debug (form body))) `(let ((buffer (generate-new-buffer " *etaf-dynamic-test*")) (etaf-dynamic-test-setups nil) (etaf-dynamic-test-disposals nil) (etaf-dynamic-test-hooks nil) (etaf-dynamic-test-actions nil)) (unwind-protect (progn (etaf-mount buffer ,view) (let ((runtime (etaf-runtime-for-buffer buffer))) ,@body)) (etaf-dynamic-test--close buffer)))) (etaf-define-component etaf-dynamic-test-snapshot-owner (&key label rows width palette calls) :setup (etaf-theme-provide palette) :render (progn (cl-incf (aref calls 0)) (etaf-view (column :width (etaf-value width) :color (etaf-theme-token :fg) (text (expr (progn (cl-incf (aref calls 1)) (etaf-value label)))) (expr (progn (cl-incf (aref calls 2)) (mapcar (lambda (value) (etaf-node 'text nil (list value))) (etaf-value rows)))))))) (defun etaf-dynamic-test--snapshot-text (node) "Return the material text strings below public canonical NODE." (if (ebox-text-node-p node) (list (substring-no-properties (ebox-text-node-value node))) (mapcan #'etaf-dynamic-test--snapshot-text (ebox-box-node-children node)))) (defun etaf-dynamic-test--paint-foreground (buffer color) "Read COLOR's currently published foreground in BUFFER." (if (tp-paint-slot-p color) (with-current-buffer buffer (plist-get (cadr (assq (tp-paint-slot-face color) face-remapping-alist)) :foreground)) color)) (ert-deftest etaf-dynamic-components-snapshot-observes-local-publications () "Explicit exports observe Text, props, Range, and Theme without evaluating." (let ((label (etaf-ref "A")) (rows (etaf-ref '("R1"))) (width (etaf-ref '(120))) (palette (etaf-ref '(:fg "#111111"))) (calls (vector 0 0 0))) (etaf-dynamic-test--with-runtime (etaf-view (etaf-dynamic-test-snapshot-owner :label label :rows rows :width width :palette palette :calls calls)) (cl-labels ((check (expected-text expected-width expected-color) (let ((generation (etaf-runtime-current-generation runtime)) (revision (etaf-render-port-revision buffer)) (before (copy-sequence calls)) snapshot) (cl-letf (((symbol-function 'etaf-render-port-update) (lambda (&rest _) (error "Snapshot published"))) ((symbol-function 'etaf--runtime-render-root-turn) (lambda (&rest _) (error "Snapshot rebuilt Root")))) (setq snapshot (etaf-runtime-snapshot runtime))) (should (eq generation (etaf-runtime-current-generation runtime))) (should (= revision (plist-get snapshot :revision))) (should (= revision (etaf-render-port-revision buffer))) (should (equal before calls)) (should (= 1 (aref calls 0))) (let* ((input (plist-get snapshot :input)) (root (car (ebox-canonical-input-roots input))) (color (ebox-style-node-specified-value input :color))) (should (equal expected-text (etaf-dynamic-test--snapshot-text root))) (should (equal expected-width (ebox-style-node-specified-value input :width))) (should (equal expected-color (etaf-dynamic-test--paint-foreground buffer color)))) snapshot))) (let* ((initial (check '("A" "R1") '(120) "#111111")) (initial-input (plist-get initial :input))) (setf (etaf-value label) "B") (check '("B" "R1") '(120) "#111111") (setf (etaf-value width) '(140)) (check '("B" "R1") '(140) "#111111") (setf (etaf-value rows) '("R2" "R3")) (check '("B" "R2" "R3") '(140) "#111111") (setf (etaf-value palette) '(:fg "#eeeeee")) (let ((current (check '("B" "R2" "R3") '(140) "#eeeeee"))) (should (= (plist-get initial :mount-id) (plist-get current :mount-id))) (should (string-match-p "R3" (ebox-render (plist-get current :input))))) (cl-letf (((symbol-function 'etaf--runtime-swap-generation) (lambda (&rest _) (error "Reject Host property candidate")))) (should-error (setf (etaf-value width) '(160)))) (check '("B" "R2" "R3") '(140) "#eeeeee") (setf (etaf-value width) '(140)) (should (equal '("A" "R1") (etaf-dynamic-test--snapshot-text (car (ebox-canonical-input-roots initial-input))))) (should (equal '(120) (ebox-style-node-specified-value initial-input :width))) ;; The obsolete getter remains a current read, without a stored mirror. (should (equal '("B" "R2" "R3") (etaf-dynamic-test--snapshot-text (funcall 'etaf-runtime-root-node runtime))))))))) (etaf-define-component etaf-dynamic-test-theme-projection (&key width palette transform calls) :setup (etaf-theme-provide palette) :render (progn (cl-incf (aref calls 0)) (etaf-view (column :width (if width (etaf-value width) '(120)) :color (etaf-theme-token :fg nil transform) (text "Content"))))) (defun etaf-dynamic-test--snapshot-color (runtime) "Read RUNTIME's canonical root color through the public snapshot API." (ebox-style-node-specified-value (plist-get (etaf-runtime-snapshot runtime) :input) :color)) (ert-deftest etaf-dynamic-components-host-projection-retains-paint-slot () "Compiled width changes retain the original Theme paint contribution." (require 'bytecomp) (let* ((width (etaf-ref '(120))) (palette (etaf-ref '(:fg "#112233"))) (calls (vector 0)) (change-width (byte-compile (lambda (next) (setf (etaf-value width) next))))) (etaf-dynamic-test--with-runtime (etaf-view (etaf-dynamic-test-theme-projection :width width :palette palette :calls calls)) (let ((slot (etaf-dynamic-test--snapshot-color runtime))) (should (tp-paint-slot-p slot)) (cl-letf (((symbol-function 'tp-paint-slot-create) (lambda (&rest _) (ert-fail "Reprojection allocated a paint slot")))) (funcall change-width '(140))) ;; Ebox declarations compare opaque TP paint addresses by their face, ;; not the copied record's installation/spec publication metadata. (should (eq (tp-paint-slot-face slot) (tp-paint-slot-face (etaf-dynamic-test--snapshot-color runtime)))) (setf (etaf-value palette) '(:fg "#445566")) (should (eq (tp-paint-slot-face slot) (tp-paint-slot-face (etaf-dynamic-test--snapshot-color runtime)))) (should (equal "#445566" (etaf-dynamic-test--paint-foreground buffer slot))) (should (= 1 (aref calls 0))))))) (ert-deftest etaf-dynamic-components-host-projection-collects-transform-reads () "A transform's new reactive reads belong to the reprojected Host effect." (require 'bytecomp) (let* ((width (etaf-ref '(120))) (palette (etaf-ref '(:fg "#112233"))) (extra (etaf-ref "#445566")) (flags (vector nil)) (calls (vector 0)) (transform (byte-compile (lambda (value) (if (aref flags 0) (etaf-value extra) value))))) (etaf-dynamic-test--with-runtime (etaf-view (etaf-dynamic-test-theme-projection :width width :palette palette :transform transform :calls calls)) (aset flags 0 t) (setf (etaf-value width) '(140)) (setf (etaf-value extra) "#778899") (let ((color (etaf-dynamic-test--snapshot-color runtime))) (should (equal "#778899" (etaf-dynamic-test--paint-foreground buffer color)))) (should (= 1 (aref calls 0)))))) (ert-deftest etaf-dynamic-components-host-projection-rejects-transform-writes () "Both property and Theme-only turns reject writes before any publication." (require 'bytecomp) (dolist (property-turn '(nil t)) (dolist (reject-publication '(nil t)) (let* ((width (and property-turn (etaf-ref '(120)))) (palette (etaf-ref '(:fg "#112233"))) (target (etaf-ref 0)) (flags (vector nil)) (calls (vector 0)) (transform (byte-compile (lambda (value) (when (aref flags 0) (setf (etaf-value target) 1)) value))) (change (byte-compile (lambda () (if width (setf (etaf-value width) '(140)) (setf (etaf-value palette) '(:fg "#445566"))))))) (etaf-dynamic-test--with-runtime (etaf-view (etaf-dynamic-test-theme-projection :width width :palette palette :transform transform :calls calls)) (let ((generation (etaf-runtime-current-generation runtime)) (revision (etaf-render-port-revision buffer)) (published (with-current-buffer buffer (buffer-string))) (slot (etaf-dynamic-test--snapshot-color runtime))) (aset flags 0 t) (if reject-publication (cl-letf (((symbol-function 'etaf--runtime-swap-generation) (lambda (&rest _) (error "Reject candidate")))) (should-error (funcall change) :type 'etaf-render-write-error)) (should-error (funcall change) :type 'etaf-render-write-error)) (should (= 0 (etaf-value target))) (should (eq generation (etaf-runtime-current-generation runtime))) (should (= revision (etaf-render-port-revision buffer))) (should (equal-including-properties published (with-current-buffer buffer (buffer-string)))) (should (eq slot (etaf-dynamic-test--snapshot-color runtime))) (should (equal "#112233" (etaf-dynamic-test--paint-foreground buffer slot))) (aset flags 0 nil) (if width (setf (etaf-value width) '(120)) (setf (etaf-value palette) '(:fg "#112233"))) (should (= 1 (aref calls 0))))))))) (ert-deftest etaf-dynamic-components-snapshot-rejects-uncommitted-lifecycle () "Unmounted and transactional reads fail; detached exports survive remount." (with-temp-buffer (should-error (etaf-runtime-snapshot) :type 'etaf-runtime-error)) (etaf-dynamic-test--with-runtime (etaf-view (text "Original")) (let* ((snapshot (with-current-buffer buffer (etaf-runtime-snapshot))) (revision (plist-get snapshot :revision)) (generation (etaf-runtime-current-generation runtime))) (tp-with-transaction (should-error (etaf-runtime-snapshot runtime))) (should (= revision (etaf-render-port-revision buffer))) (should (eq generation (etaf-runtime-current-generation runtime))) (etaf-unmount runtime) (cl-letf (((symbol-function 'etaf-render-port-snapshot) (lambda (&rest _) (ert-fail "Unmounted query reached Ebox")))) (should-error (etaf-runtime-snapshot runtime) :type 'etaf-runtime-error) (should-error (etaf-runtime-flush runtime) :type 'etaf-runtime-error)) (etaf-mount buffer (etaf-view (text "Replacement"))) (let ((replacement (etaf-runtime-snapshot (etaf-runtime-for-buffer buffer)))) (should-not (= (plist-get snapshot :mount-id) (plist-get replacement :mount-id)))) (should (equal '("Original") (etaf-dynamic-test--snapshot-text (car (ebox-canonical-input-roots (plist-get snapshot :input))))))))) (ert-deftest etaf-dynamic-components-snapshot-does-not-drain-batched-inputs () "A query observes the last publication while a reactive batch is pending." (let ((label (etaf-ref "Before"))) (etaf-dynamic-test--with-runtime (etaf-view (text (expr (etaf-value label)))) (let ((revision (etaf-render-port-revision buffer))) (etaf-reactive-call-with-batch (lambda () (setf (etaf-value label) "After") (let ((snapshot (etaf-runtime-snapshot runtime))) (should (= revision (plist-get snapshot :revision))) (should (equal '("Before") (etaf-dynamic-test--snapshot-text (car (ebox-canonical-input-roots (plist-get snapshot :input))))))))) (should (equal "After" (etaf-dynamic-test--text buffer))) (should (> (etaf-render-port-revision buffer) revision)))))) (ert-deftest etaf-dynamic-components-flush-busy-returns-committed-revision () "A busy flush returns the last publication and never implicitly exports." (let ((label (etaf-ref "Before"))) (etaf-dynamic-test--with-runtime (etaf-view (text (expr (etaf-value label)))) (let ((revision (etaf-render-port-revision buffer))) (cl-letf (((symbol-function 'etaf-runtime-snapshot) (lambda (&rest _) (ert-fail "Flush exported Runtime"))) ((symbol-function 'etaf-render-port-snapshot) (lambda (&rest _) (ert-fail "Flush exported Ebox"))) ((symbol-function 'etaf--runtime-render-root-turn) (lambda (&rest _) (ert-fail "Flush rebuilt Root")))) (should (= revision (etaf-runtime-flush runtime))) (unwind-protect (progn (setf (etaf-runtime-event-depth runtime) 1) (setf (etaf-value label) "After") (should (= revision (etaf-runtime-flush runtime))) (should (etaf-runtime-pending-p runtime)) (should (equal "Before" (etaf-dynamic-test--text buffer)))) (setf (etaf-runtime-event-depth runtime) 0)) (should (> (etaf-runtime-flush runtime) revision)) (should (equal "After" (etaf-dynamic-test--text buffer))) (should (= (etaf-render-port-revision buffer) (etaf-runtime-flush runtime)))))))) (ert-deftest etaf-dynamic-components-removed-slot-selects-fallback () (let ((source (etaf-ref (etaf-node 'etaf-dynamic-test-slot-inner nil '("Supplied"))))) (etaf-dynamic-test--with-runtime (lambda () (etaf-value source)) (should (equal "Supplied" (etaf-dynamic-test--text buffer))) (setf (etaf-value source) (etaf-node 'etaf-dynamic-test-slot-inner nil nil)) (should (equal "FALLBACK" (etaf-dynamic-test--text buffer)))))) (ert-deftest etaf-dynamic-components-empty-candidate-props-and-slots () "Slot programs observe present empty inputs in their caller's environment." (let* ((children (list "Supplied")) (source (etaf-ref (etaf-node 'etaf-dynamic-test-slot-outer '(:label "A") children)))) (etaf-dynamic-test--with-runtime (lambda () (etaf-value source)) (should (string-match-p "A.*HAS-SLOT" (etaf-dynamic-test--text buffer))) (setf (etaf-value source) (etaf-node 'etaf-dynamic-test-slot-outer nil children)) (should (string-match-p "EMPTY.*HAS-SLOT" (etaf-dynamic-test--text buffer))) (setf (etaf-value source) (etaf-node 'etaf-dynamic-test-slot-outer nil nil)) (should (string-match-p "EMPTY.*NO-SLOT" (etaf-dynamic-test--text buffer))) (etaf-dispatch-event runtime 'etaf-dynamic-slot-snapshot 'press) (should (equal '((nil nil)) etaf-dynamic-test-actions))))) (ert-deftest etaf-dynamic-components-empty-candidate-inputs-rollback () "Rejected empty inputs leave the published caller snapshot intact." (let* ((original (etaf-node 'etaf-dynamic-test-slot-outer '(:label "A") '("Supplied"))) (source (etaf-ref original))) (etaf-dynamic-test--with-runtime (lambda () (etaf-value source)) (let ((generation (etaf-runtime-current-generation runtime)) (published (with-current-buffer buffer (buffer-string)))) (cl-letf (((symbol-function 'etaf--runtime-swap-generation) (lambda (&rest _) (error "Reject empty input candidate")))) (should-error (setf (etaf-value source) (etaf-node 'etaf-dynamic-test-slot-outer nil nil)))) (should (eq generation (etaf-runtime-current-generation runtime))) (should (equal-including-properties published (with-current-buffer buffer (buffer-string)))) (etaf-dispatch-event runtime 'etaf-dynamic-slot-snapshot 'press) (should (equal '(("A" t)) etaf-dynamic-test-actions)) (setf (etaf-value source) original) (setf (etaf-value source) (etaf-node 'etaf-dynamic-test-slot-outer nil nil)) (should (string-match-p "EMPTY.*NO-SLOT" (etaf-dynamic-test--text buffer))))))) (ert-deftest etaf-dynamic-components-root-slot-context-matches-component-author () "Root-owned slots use root Context while ordinary factories use consumers." (dolist (wrapped '(nil t)) (let ((label (etaf-ref "A")) (cell (lambda () (etaf-view (etaf-dynamic-test-context-reader))))) (etaf-dynamic-test--with-runtime (if wrapped (etaf-view (etaf-dynamic-test-context-author :label label :cell cell)) (etaf-view (etaf-dynamic-test-context-provider :cell cell (text (expr (concat (etaf-inject 'etaf-dynamic-slot-context "ROOT") ":" (etaf-value label)))) (etaf-dynamic-test-context-reader)))) (should (string-match-p "\\`ROOT:A[[:space:]]+ROOT[[:space:]]+INNER[[:space:]]*\\'" (etaf-dynamic-test--text buffer))) (setf (etaf-value label) "B") (should (string-match-p "\\`ROOT:B[[:space:]]+ROOT[[:space:]]+INNER[[:space:]]*\\'" (etaf-dynamic-test--text buffer))) (let ((generation (etaf-runtime-current-generation runtime)) (published (with-current-buffer buffer (buffer-string)))) (should-error (setf (etaf-value label) 42)) (should (eq generation (etaf-runtime-current-generation runtime))) (should (equal-including-properties published (with-current-buffer buffer (buffer-string))))) (setf (etaf-value label) "C") (should (string-match-p "\\`ROOT:C[[:space:]]+ROOT[[:space:]]+INNER[[:space:]]*\\'" (etaf-dynamic-test--text buffer))))))) (ert-deftest etaf-dynamic-components-retain-singleton-sequence-state () (let* ((item (etaf-dynamic-test--counter "A")) (source (etaf-ref item))) (etaf-dynamic-test--with-runtime (etaf-view (etaf-dynamic-test-owner :source source)) (let ((state (cdar etaf-dynamic-test-setups))) (setf (etaf-value state) 3) (setf (etaf-value source) (list item (etaf-dynamic-test--counter "B"))) (should (= 2 (length etaf-dynamic-test-setups))) (should (string-match-p "A:3" (etaf-dynamic-test--text buffer))) (setf (etaf-value source) item) (should (= 2 (length etaf-dynamic-test-setups))) (should (equal '("B") etaf-dynamic-test-disposals)) (should (string-match-p "A:3" (etaf-dynamic-test--text buffer))) (setf (etaf-value source) nil) (should (equal '("A" "B") etaf-dynamic-test-disposals)) (should (equal 1 (cl-count '("A" . unmounted) etaf-dynamic-test-hooks :test #'equal))))))) (ert-deftest etaf-dynamic-components-fragment-composes-material-components () (let ((source (etaf-ref (etaf-node 'etaf-dynamic-test-pair nil nil)))) (etaf-dynamic-test--with-runtime (etaf-view (etaf-dynamic-test-owner :source source)) (should (= 2 (length etaf-dynamic-test-setups))) (setf (etaf-value (cdr (assoc "A" etaf-dynamic-test-setups))) 3) (should (string-match-p "A:3" (etaf-dynamic-test--text buffer))) (should (string-match-p "B:0" (etaf-dynamic-test--text buffer))) (setf (etaf-value source) nil) (should (= 2 (length etaf-dynamic-test-disposals)))))) (ert-deftest etaf-dynamic-components-material-child-props-update-retains-hosts () (let* ((label (etaf-ref "A")) (source (etaf-ref (etaf-view (etaf-dynamic-test-wrapper :label (etaf-value label)))))) (etaf-dynamic-test--with-runtime (etaf-view (etaf-dynamic-test-owner :source source)) (setf (etaf-value (cdar etaf-dynamic-test-setups)) 4) (setf (etaf-value label) "B") (should (= 1 (length etaf-dynamic-test-setups))) (should-not etaf-dynamic-test-disposals) (should (string-match-p "B:4" (etaf-dynamic-test--text buffer)))))) (ert-deftest etaf-dynamic-components-keyed-first-update-retains-programmatic-hosts () "Initial and incremental keyed items use the same descendant positions." (let ((items (etaf-ref '((one . "A"))))) (etaf-dynamic-test--with-runtime (etaf-view (column (etaf-dynamic-test-programmatic-keyed-item :for (entry (etaf-value items)) :key (car entry) :entry entry))) (let* ((ref (caar (etaf-runtime-handler-entries runtime))) (ancestry (gethash ref (etaf-runtime-host-ancestries runtime (list ref))))) (etaf-dispatch-event runtime ref 'press) (should (string-match-p "A:1" (etaf-dynamic-test--text buffer))) (dolist (label '("B" "C")) (setf (etaf-value items) (list (cons 'one label))) (should (= 1 (length etaf-dynamic-test-setups))) (should-not etaf-dynamic-test-disposals) (should (equal ref (caar (etaf-runtime-handler-entries runtime)))) (should (equal ancestry (gethash ref (etaf-runtime-host-ancestries runtime (list ref))))) (should (string-match-p (concat label ":1") (etaf-dynamic-test--text buffer)))) (etaf-dispatch-event runtime ref 'press) (should (equal '("C" "A") etaf-dynamic-test-actions)) (should (string-match-p "C:2" (etaf-dynamic-test--text buffer))))))) (defun etaf-dynamic-test--keyed-fragment-view (source &optional stateful) "Return keyed forests from SOURCE, optionally containing STATEFUL counters." (etaf-view (column (fragment :for (entry (etaf-value source)) :key (car entry) (expr (mapcar (lambda (label) (cond ((eq label 'fail) (etaf-node 'etaf-dynamic-test-failure nil nil)) (stateful (etaf-dynamic-test--counter label)) (t (etaf-node 'text nil (list label))))) (cdr entry))))))) (ert-deftest etaf-dynamic-components-keyed-fragment-mount-matches-pure () "Empty, single and multiple roots remain grouped by their logical key." (dolist (items '(((only)) ((only "A")) ((only "A" "B")) ((empty) (pair "A" "B") (single "C")))) (let* ((source (etaf-ref items)) (view (etaf-dynamic-test--keyed-fragment-view source)) (pure (substring-no-properties (ebox-render (etaf-render view))))) (etaf-dynamic-test--with-runtime view (should (equal pure (etaf-dynamic-test--text buffer))) (dolist (next '(((only)) ((only "one")) ((only "left" "right")) ((only)))) (setf (etaf-value source) next) (should (equal (substring-no-properties (ebox-render (etaf-render (etaf-dynamic-test--keyed-fragment-view source)))) (etaf-dynamic-test--text buffer)))))))) (ert-deftest etaf-dynamic-components-keyed-fragment-spans-retain-state () "Per-key forests retain state as items grow, shrink, reorder or fail." (let ((source (etaf-ref '((empty) (pair "A" "B") (single "C"))))) (etaf-dynamic-test--with-runtime (etaf-dynamic-test--keyed-fragment-view source t) (let* ((generation (etaf-runtime-current-generation runtime)) (effect-id (car (etaf--generation-source-effects generation source))) (range (etaf--generation-effect-semantic generation effect-id)) (spans (etaf--semantic-range-keyed-item-node-span-index range))) ;; Total output cardinality happens to equal item count here. Each ;; logical key must still own its own zero-, two-, or one-node span. (should (equal '(0 . 0) (gethash 'empty spans))) (should (equal '(0 . 2) (gethash 'pair spans))) (should (equal '(2 . 1) (gethash 'single spans)))) (should (= 3 (length etaf-dynamic-test-setups))) (setf (etaf-value (cdr (assoc "A" etaf-dynamic-test-setups))) 7) (let* ((refs (mapcar #'car (etaf-runtime-handler-entries runtime))) (ancestries (etaf-runtime-host-ancestries runtime refs)) (a-ref (cl-find-if (lambda (ref) (let ((bounds (etaf-host-ref-bounds runtime ref))) (with-current-buffer buffer (string-match-p "A:7" (buffer-substring-no-properties (car bounds) (cdr bounds)))))) refs))) (should a-ref) (setf (etaf-value source) '((single "C") (pair "A" "B") (empty))) (should (equal '("C:0" "A:7" "B:0") (split-string (etaf-dynamic-test--text buffer)))) (should (= 3 (length etaf-dynamic-test-setups))) (setf (etaf-value source) '((single "C") (pair "A2" "B2") (empty "D"))) (should (= 4 (length etaf-dynamic-test-setups))) (dolist (ref refs) (should (equal (gethash ref ancestries) (gethash ref (etaf-runtime-host-ancestries runtime refs))))) (setf (etaf-value source) '((single "C") (pair "A3") (empty "D"))) (should (equal '("B") etaf-dynamic-test-disposals)) (setf (etaf-value source) '((single "C") (pair "A4" "B3") (empty "D"))) (should (= 5 (length etaf-dynamic-test-setups))) (should (equal '("C:0" "A4:7" "B3:0" "D:0") (split-string (etaf-dynamic-test--text buffer)))) (let ((generation (etaf-runtime-current-generation runtime)) (published (with-current-buffer buffer (buffer-string)))) (should-error (setf (etaf-value source) '((new "N") (pair "A5" fail) (single "C") (empty "D")))) (should (eq generation (etaf-runtime-current-generation runtime))) (should (equal-including-properties published (with-current-buffer buffer (buffer-string)))) (should (member "N" etaf-dynamic-test-disposals)) (should-not (member '("N" . mounted) etaf-dynamic-test-hooks))) (setf (etaf-value source) '((single "C") (pair "A4" "B3") (empty "D"))) (should (equal (gethash a-ref ancestries) (gethash a-ref (etaf-runtime-host-ancestries runtime (list a-ref))))) (etaf-dispatch-event runtime a-ref 'press) (should (equal '("A4") etaf-dynamic-test-actions)) (should (string-match-p "A4:8" (etaf-dynamic-test--text buffer))) (setf (etaf-value source) '((single "C") (empty "D"))) (should-not (etaf-runtime-handler-for runtime a-ref)) (setf (etaf-value source) nil) (should (equal "" (etaf-dynamic-test--text buffer))) (should (equal '("A" "B" "B3" "C" "D" "N") (sort (copy-sequence etaf-dynamic-test-disposals) #'string<))))))) (ert-deftest etaf-dynamic-components-root-owned-expression-updates () (let ((source (etaf-ref nil))) (etaf-dynamic-test--with-runtime (etaf-view (column (expr (etaf-value source)))) (setf (etaf-value source) (etaf-dynamic-test--counter "root")) (setf (etaf-value (cdar etaf-dynamic-test-setups)) 2) (should (string-match-p "root:2" (etaf-dynamic-test--text buffer))) (setf (etaf-value source) nil) (should (equal '("root") etaf-dynamic-test-disposals))))) (ert-deftest etaf-dynamic-components-remount-then-update-reuses-new-instance () (let ((source (etaf-ref (etaf-dynamic-test--counter "A" 'a)))) (etaf-dynamic-test--with-runtime (etaf-view (etaf-dynamic-test-owner :source source)) (setf (etaf-value source) nil) (setf (etaf-value source) (etaf-dynamic-test--counter "A" 'a)) (let ((state (cdar etaf-dynamic-test-setups))) (should (= 2 (length etaf-dynamic-test-setups))) (setf (etaf-value state) 5) (setf (etaf-value source) (etaf-dynamic-test--counter "A2" 'a)) (should (= 2 (length etaf-dynamic-test-setups))) (should (string-match-p "A2:5" (etaf-dynamic-test--text buffer))))))) (ert-deftest etaf-dynamic-components-keyed-reorder-remove-and-duplicate () (let ((source (etaf-ref (list (etaf-dynamic-test--counter "A" 'a) (etaf-dynamic-test--counter "B" 'b))))) (etaf-dynamic-test--with-runtime (etaf-view (etaf-dynamic-test-owner :source source)) (let ((state-a (cdr (assoc "A" etaf-dynamic-test-setups)))) (setf (etaf-value state-a) 7) (setf (etaf-value source) (list (etaf-dynamic-test--counter "B2" 'b) (etaf-dynamic-test--counter "A2" 'a))) (should (= 2 (length etaf-dynamic-test-setups))) (should (string-match-p "A2:7" (etaf-dynamic-test--text buffer))) (let ((generation (etaf-runtime-current-generation runtime)) (text (etaf-dynamic-test--text buffer))) (should-error (setf (etaf-value source) (list (etaf-dynamic-test--counter "bad" 'a) (etaf-dynamic-test--counter "duplicate" 'a))) :type 'etaf-runtime-error) (should (eq generation (etaf-runtime-current-generation runtime))) (should (equal text (etaf-dynamic-test--text buffer)))))))) (ert-deftest etaf-dynamic-components-reused-description-isolates-instances () (let* ((item (etaf-dynamic-test--counter "shared-description")) (source (etaf-ref (list item item)))) (etaf-dynamic-test--with-runtime (etaf-view (etaf-dynamic-test-owner :source source)) (should (= 2 (length etaf-dynamic-test-setups))) (let ((states (mapcar #'cdr etaf-dynamic-test-setups))) (should-not (eq (car states) (cadr states))) (setf (etaf-value (car states)) 8) (should (string-match-p "shared-description:8" (etaf-dynamic-test--text buffer))) (should (string-match-p "shared-description:0" (etaf-dynamic-test--text buffer))))))) (defun etaf-dynamic-test--helper (source) "Return a reused compiled Host and Expr site reading SOURCE." (etaf-view (column (expr (etaf-value source))))) (defun etaf-dynamic-test--expr-helper (source) "Return a reused expression site with no intervening Host." (etaf-view (expr (etaf-value source)))) (defun etaf-dynamic-test--deferred-helper (left right calls) "Return independent LEFT/RIGHT text programs counted in CALLS." (etaf-view (column (text (expr (progn (cl-incf (aref calls 0)) (etaf-value left)))) (text (expr (progn (cl-incf (aref calls 1)) (etaf-value right))))))) (defun etaf-dynamic-test--lexical-helper (value) "Return inline and structural programs capturing the current VALUE." (etaf-view (column (text :ref 'lexical-inline (expr value)) (column (expr (etaf-node 'text (list :ref 'lexical-structural) (list value))))))) (ert-deftest etaf-dynamic-components-parent-range-retargets-lexical-programs () "Fresh lexical inputs update stable child Range owners and roll back errors." (let ((source (etaf-ref "Before"))) (etaf-dynamic-test--with-runtime (etaf-view (column (expr (etaf-dynamic-test--lexical-helper (etaf-value source))))) (let* ((generation (etaf-runtime-current-generation runtime)) (inline-ancestry (gethash 'lexical-inline (etaf-runtime-host-ancestries runtime '(lexical-inline)))) (structural-ancestry (gethash 'lexical-structural (etaf-runtime-host-ancestries runtime '(lexical-structural)))) (published (with-current-buffer buffer (buffer-string)))) (should (string-match-p "Before[[:space:]]+Before" (etaf-dynamic-test--text buffer))) (should-error (setf (etaf-value source) 42)) (should (eq generation (etaf-runtime-current-generation runtime))) (should (equal-including-properties published (with-current-buffer buffer (buffer-string)))) (setf (etaf-value source) "After") (should (string-match-p "After[[:space:]]+After" (etaf-dynamic-test--text buffer))) (dolist (entry (list (cons 'lexical-inline inline-ancestry) (cons 'lexical-structural structural-ancestry))) (should (equal (cdr entry) (gethash (car entry) (etaf-runtime-host-ancestries runtime (list (car entry))))))))))) (defun etaf-dynamic-test--batch-lexical-helper (value source calls kind) "Return KIND programs combining lexical VALUE and reactive SOURCE." (etaf-node 'column nil (append (when (memq kind '(inline both)) (list (etaf-view (text :ref 'batch-inline (expr (progn (cl-incf (aref calls 1)) (concat value "/" (etaf-value source)))))))) (when (memq kind '(structural both)) (list (etaf-view (column (expr (progn (cl-incf (aref calls 2)) (etaf-node 'text (list :ref 'batch-structural) (list (concat value "/" (etaf-value source))))))))))))) (defun etaf-dynamic-test--check-parent-child-batch (kind) "Check KIND nested programs for both parent/child notification orders." (dolist (child-first '(nil t)) (let ((outer (etaf-ref "A")) (inner (etaf-ref "1")) (calls (vector 0 0 0))) (etaf-dynamic-test--with-runtime (etaf-view (column (expr (progn (cl-incf (aref calls 0)) (when (etaf-value outer) (etaf-dynamic-test--batch-lexical-helper (etaf-value outer) inner calls kind)))))) (let* ((refs (pcase kind ('inline '(batch-inline)) ('structural '(batch-structural)) (_ '(batch-inline batch-structural)))) (ancestries (etaf-runtime-host-ancestries runtime refs)) (version (etaf-runtime-generation runtime))) (cl-labels ((check-output (value) (should (equal (split-string (etaf-dynamic-test--text buffer)) (make-list (length refs) value)))) (update (parent child) (etaf-reactive-call-with-batch (lambda () (if child-first (setf (etaf-value inner) child (etaf-value outer) parent) (setf (etaf-value outer) parent (etaf-value inner) child)))))) (check-output "A/1") (update "B" "2") (check-output "B/2") (should (= (1+ version) (etaf-runtime-generation runtime))) (should (equal calls (pcase kind ('inline [2 2 0]) ('structural [2 0 2]) (_ [2 2 2])))) (dolist (ref refs) (should (equal (gethash ref ancestries) (gethash ref (etaf-runtime-host-ancestries runtime refs))))) (let ((generation (etaf-runtime-current-generation runtime)) (published (with-current-buffer buffer (buffer-string)))) (should-error (update 42 "3")) (should (eq generation (etaf-runtime-current-generation runtime))) (should (equal-including-properties published (with-current-buffer buffer (buffer-string))))) (update "C" "4") (check-output "C/4") (let ((parent-calls (aref calls 0))) (setf (etaf-value inner) "5") (check-output "C/5") (should (= parent-calls (aref calls 0)))) (let ((child-calls (list (aref calls 1) (aref calls 2)))) (update nil "6") (should (equal "" (etaf-dynamic-test--text buffer))) (should (equal child-calls (list (aref calls 1) (aref calls 2))))))))))) (ert-deftest etaf-dynamic-components-parent-child-batch-inline () "Inline descendants use the fresh parent closure once in a shared batch." (etaf-dynamic-test--check-parent-child-batch 'inline)) (ert-deftest etaf-dynamic-components-parent-child-batch-structural () "Structural descendants use one coherent candidate in a shared batch." (etaf-dynamic-test--check-parent-child-batch 'structural)) (ert-deftest etaf-dynamic-components-parent-child-batch-mixed () "Mixed descendants retain ownership through batches, rollback and removal." (etaf-dynamic-test--check-parent-child-batch 'both)) (ert-deftest etaf-dynamic-components-helper-expr-keeps-dependencies-local () "Direct and structural-expression use retain the same child dependencies." (dolist (wrapped '(nil t)) (let ((left (etaf-ref "Left")) (right (etaf-ref "Right")) (calls (vector 0 0))) (etaf-dynamic-test--with-runtime (if wrapped (etaf-view (column (expr (etaf-dynamic-test--deferred-helper left right calls)))) (etaf-dynamic-test--deferred-helper left right calls)) (should (equal calls [1 1])) (setf (etaf-value left) "LEFT") (should (equal calls [2 1])) (should (string-match-p "LEFT[[:space:]]+Right" (etaf-dynamic-test--text buffer))) (setf (etaf-value right) "RIGHT") (should (equal calls [2 2])) (let ((generation (etaf-runtime-current-generation runtime)) (published (with-current-buffer buffer (buffer-string)))) (should-error (setf (etaf-value left) 42)) (should (eq generation (etaf-runtime-current-generation runtime))) (should (equal-including-properties published (with-current-buffer buffer (buffer-string)))) (should (= 2 (aref calls 1)))))))) (defun etaf-dynamic-test--keyed-helper (left right) "Return two independent keyed Component ranges over LEFT and RIGHT." (etaf-view (column (etaf-dynamic-test-counter :for (item (etaf-value left)) :key item :label "A") (etaf-dynamic-test-counter :for (item (etaf-value right)) :key item :label "B")))) (ert-deftest etaf-dynamic-components-helper-expr-keeps-keyed-scopes () "Equal keys at distinct helper sites own separate state and disposal." (dolist (wrapped '(nil t)) (let ((left (etaf-ref '(1))) (right (etaf-ref '(1)))) (etaf-dynamic-test--with-runtime (if wrapped (etaf-view (column (expr (etaf-dynamic-test--keyed-helper left right)))) (etaf-dynamic-test--keyed-helper left right)) (should (= 2 (length etaf-dynamic-test-setups))) (let ((state-a (cdr (assoc "A" etaf-dynamic-test-setups))) (state-b (cdr (assoc "B" etaf-dynamic-test-setups)))) (should-not (eq state-a state-b)) (setf (etaf-value state-a) 7) (should (string-match-p "A:7[[:space:]]+B:0" (etaf-dynamic-test--text buffer))) (setf (etaf-value left) nil) (should (equal '("A") etaf-dynamic-test-disposals)) (setf (etaf-value state-b) 8) (should (string-match-p "B:8" (etaf-dynamic-test--text buffer))) (should (= 2 (length etaf-dynamic-test-setups)))))))) (ert-deftest etaf-dynamic-components-reused-expression-sites-remain-independent () (let ((left (etaf-ref (etaf-dynamic-test--counter "left"))) (right (etaf-ref (etaf-dynamic-test--counter "right")))) (etaf-dynamic-test--with-runtime (etaf-node 'column nil (list (etaf-dynamic-test--expr-helper left) (etaf-dynamic-test--expr-helper right))) (should (= 2 (length etaf-dynamic-test-setups))) (setf (etaf-value left) nil) (should (equal '("left") etaf-dynamic-test-disposals)) (should (string-match-p "right:0" (etaf-dynamic-test--text buffer)))))) (ert-deftest etaf-dynamic-components-helper-sites-isolate-hosts-and-ranges () (let ((left (etaf-ref (etaf-dynamic-test--counter "left"))) (right (etaf-ref (etaf-dynamic-test--counter "right")))) (etaf-dynamic-test--with-runtime (etaf-node 'column nil (list (etaf-dynamic-test--helper left) (etaf-dynamic-test--helper right))) (should (= 2 (length etaf-dynamic-test-setups))) (setf (etaf-value left) nil) (should (equal '("left") etaf-dynamic-test-disposals)) (should (string-match-p "right:0" (etaf-dynamic-test--text buffer)))))) (ert-deftest etaf-dynamic-components-invalid-output-has-public-context () (let ((source (etaf-ref nil))) (etaf-dynamic-test--with-runtime (etaf-view (etaf-dynamic-test-owner :source source)) (let ((condition (should-error (setf (etaf-value source) '(text "raw")) :type 'etaf-runtime-error))) (should (string-match-p "etaf-dynamic-test-owner" (error-message-string condition))) (should (string-match-p "View" (error-message-string condition))) (should-not (string-match-p "Step4" (error-message-string condition))))))) (ert-deftest etaf-dynamic-components-nested-shapes-and-type-replacement () (let ((source (etaf-ref nil))) (etaf-dynamic-test--with-runtime (etaf-view (etaf-dynamic-test-owner :source source)) (dolist (shape (list "bare" (etaf-view (text "host")) (etaf-view (fragment (text "fragment") (expr (etaf-dynamic-test--counter "nested")))))) (setf (etaf-value source) shape)) (should (string-match-p "fragment" (etaf-dynamic-test--text buffer))) (should (string-match-p "nested:0" (etaf-dynamic-test--text buffer))) (setf (etaf-value source) (etaf-dynamic-test--counter "direct" 'same)) (should (equal '("nested") etaf-dynamic-test-disposals)) (setf (etaf-value source) (etaf-node 'etaf-dynamic-test-pure '(:label "replacement" :key same) nil)) (should (equal '("direct" "nested") etaf-dynamic-test-disposals)) (should (string-match-p "replacement" (etaf-dynamic-test--text buffer)))))) (ert-deftest etaf-dynamic-components-fragment-keeps-nested-range-boundaries () (let* ((left (etaf-ref "left")) (right (etaf-ref "right")) (source (etaf-ref (etaf-node 'etaf-dynamic-test-range-pair (list :left left :right right) nil)))) (etaf-dynamic-test--with-runtime (etaf-view (etaf-dynamic-test-owner :source source)) (let* ((generation (etaf-runtime-current-generation runtime)) (pair (cl-loop for identity being the hash-keys of (etaf-generation-identity-index generation) when (eq (car-safe identity) 'etaf-dynamic-test-range-pair) return (etaf--generation-semantic generation identity)))) (should (eq 'transparent (etaf--semantic-component-publication-kind pair)))) (setf (etaf-value left) "LEFT") (should (string-match-p "LEFT" (etaf-dynamic-test--text buffer))) (setf (etaf-value right) "RIGHT") (should (string-match-p "RIGHT" (etaf-dynamic-test--text buffer)))))) (ert-deftest etaf-dynamic-components-two-apps-own-their-scopes () (let* ((item (etaf-dynamic-test--counter "A")) (source (etaf-ref item)) (view (etaf-view (etaf-dynamic-test-owner :source source)))) (etaf-dynamic-test--with-runtime view (let ((other (generate-new-buffer " *etaf-dynamic-other*")) (first-state (cdar etaf-dynamic-test-setups))) (unwind-protect (progn (etaf-mount other view) (should (= 2 (length etaf-dynamic-test-setups))) (setf (etaf-value first-state) 4) (should (string-match-p "A:4" (etaf-dynamic-test--text buffer))) (should (string-match-p "A:0" (etaf-dynamic-test--text other))) (etaf-unmount (etaf-runtime-for-buffer other)) (should (= 1 (length etaf-dynamic-test-disposals))) (setf (etaf-value first-state) 5) (should (string-match-p "A:5" (etaf-dynamic-test--text buffer)))) (etaf-dynamic-test--close other)))))) (etaf-define-component etaf-dynamic-test-failure () :render (error "dynamic sibling render failure")) (defun etaf-dynamic-test--first-handler (runtime) "Return a committed press target from RUNTIME." (caar (etaf--generation-index-entries (etaf-runtime-current-generation runtime) 'handlers))) (ert-deftest etaf-dynamic-components-failure-restores-state-handlers-and-scopes () (let ((source (etaf-ref (list (etaf-dynamic-test--counter "A" 'a))))) (etaf-dynamic-test--with-runtime (etaf-view (etaf-dynamic-test-owner :source source)) (let ((generation (etaf-runtime-current-generation runtime)) (target (etaf-dynamic-test--first-handler runtime)) (registry-count (hash-table-count (etaf-runtime-resource-registry runtime)))) (should-error (setf (etaf-value source) (list (etaf-dynamic-test--counter "B" 'a) (etaf-dynamic-test--counter "new" 'new) (etaf-node 'etaf-dynamic-test-failure nil nil))) :type 'error) (should (eq generation (etaf-runtime-current-generation runtime))) (should (= registry-count (hash-table-count (etaf-runtime-resource-registry runtime)))) (should (equal '("new") etaf-dynamic-test-disposals)) (should-not (member '("new" . mounted) etaf-dynamic-test-hooks)) ;; Restore the external source before dispatch, whose ordinary batch ;; end legitimately retries any still-dirty business value. (setf (etaf-value source) (list (etaf-dynamic-test--counter "A" 'a))) (etaf-dispatch-event runtime target :on-press) (should (equal '("A") etaf-dynamic-test-actions)) (should (string-match-p "A:1" (etaf-dynamic-test--text buffer))))))) (defun etaf-dynamic-test--keyed-host (key label) "Return one compiled keyed Host with a nested stateful LABEL Component." (etaf-view (column :key key (etaf-dynamic-test-counter :label label)))) (ert-deftest etaf-dynamic-components-keyed-host-reorder-retains-descendants () (let ((source (etaf-ref (list (etaf-dynamic-test--keyed-host 'a "A") (etaf-dynamic-test--keyed-host 'b "B"))))) (etaf-dynamic-test--with-runtime (etaf-view (etaf-dynamic-test-owner :source source)) (let* ((generation (etaf-runtime-current-generation runtime)) (host-id (cl-loop for identity being the hash-keys of (etaf--semantic-range-item-identity-index (cl-loop for identity being the hash-keys of (etaf-generation-identity-index generation) when (eq (car-safe identity) 'range) return (etaf--generation-semantic generation identity))) using (hash-values semantic-id) when (equal (plist-get (cddr identity) :key) 'a) return semantic-id)) (host-ref (etaf--semantic-host-host-ref (etaf--pvec-get (etaf-generation-semantic-nodes generation) host-id)))) (setf (etaf-value (cdr (assoc "A" etaf-dynamic-test-setups))) 6) (setf (etaf-value source) (list (etaf-dynamic-test--keyed-host 'b "B") (etaf-dynamic-test--keyed-host 'a "A"))) (should (= 2 (length etaf-dynamic-test-setups))) (should (string-match-p "A:6" (etaf-dynamic-test--text buffer))) (should-not etaf-dynamic-test-disposals) (should (equal host-ref (etaf--semantic-host-host-ref (etaf--pvec-get (etaf-generation-semantic-nodes (etaf-runtime-current-generation runtime)) host-id)))))))) (ert-deftest etaf-dynamic-components-pure-stateless-output-matches-mounted () (let* ((view (etaf-view (column (expr (list (etaf-view (etaf-dynamic-test-pure :label "A")) (etaf-view (text "B"))))))) (pure (substring-no-properties (ebox-render (etaf-render view))))) (etaf-dynamic-test--with-runtime view (should (equal pure (etaf-dynamic-test--text buffer)))))) (etaf-define-component etaf-dynamic-test-theme-alias (&key defaults property color) :setup (etaf-theme-provide defaults) :render (etaf-node 'box (list property color) (list "theme"))) (ert-deftest etaf-dynamic-components-theme-default-respects-property-alias () (dolist (case '((:bgcolor :background-color "#abcdef") (:background-color :bgcolor "#abcdef") (:bgcolor :background-color nil))) (let* ((defaults (etaf-ref (list (car case) "#123456"))) (property (cadr case)) (color (caddr case))) (etaf-dynamic-test--with-runtime (etaf-view (etaf-dynamic-test-theme-alias :defaults defaults :property property :color color)) (should (string-match-p "theme" (etaf-dynamic-test--text buffer))) (setf (etaf-value defaults) (list (car case) "#654321")) (should (string-match-p "theme" (etaf-dynamic-test--text buffer))))))) (etaf-define-component etaf-dynamic-test-theme-layout (&key palette behavior on-press) :setup (etaf-theme-provide palette) :styles (styles ("&" :padding (1 2)) (".content" :item-gap 3)) :view (column :width '(100) :height 8 :overflow 'scroll :color (etaf-theme-token :fg) :bgcolor (etaf-theme-token :bg) (row :class "content" (text :ref 'theme-layout-first :role 'button :disabled nil :use behavior :on-press on-press :color (etaf-theme-token :fg) "First") (text :ref 'theme-layout-second "Second")))) (ert-deftest etaf-dynamic-components-theme-preserves-anonymous-host-layout () "Theme-only effects retain full Host geometry without semantic attributes." (let ((palette (etaf-ref '(:fg "#111111" :bg "#ffffff")))) (etaf-dynamic-test--with-runtime (etaf-view (etaf-dynamic-test-theme-layout :palette palette)) (let* ((state (ebox--buffer-render-state buffer)) (source-index (plist-get state :source-index)) (root (plist-get state :root-node)) (geometry (cl-loop for (property value) on (ebox-tree-node-source-declarations source-index root) by #'cddr unless (memq property '(ebox/color ebox/background-color)) append (list property value))) (text (etaf-dynamic-test--text buffer)) (bounds (etaf-host-ref-bounds runtime 'theme-layout-first))) (should (equal (plist-get geometry 'ebox/width) '(100))) (should (equal (plist-get geometry 'ebox/padding-inline-start) 2)) (dolist (next '((:fg "#eeeeee" :bg "#111111") (:fg "#111111" :bg "#ffffff"))) (setf (etaf-value palette) next) (let* ((next-state (ebox--buffer-render-state buffer)) (next-root (plist-get next-state :root-node)) (next-declarations (ebox-tree-node-source-declarations (plist-get next-state :source-index) next-root))) (should (equal geometry (cl-loop for (property value) on next-declarations by #'cddr unless (memq property '(ebox/color ebox/background-color)) append (list property value)))) (should (equal text (etaf-dynamic-test--text buffer))) (should (equal bounds (etaf-host-ref-bounds runtime 'theme-layout-first))) (should (equal (with-current-buffer buffer (cl-some (lambda (spec) (and (listp spec) (plist-get spec :foreground))) (cdr (assq (tp-paint-slot-face (plist-get next-root :color)) face-remapping-alist)))) (plist-get next :fg))))))))) (ert-deftest etaf-dynamic-components-theme-retains-composed-callbacks () "Theme deltas and rollback keep callbacks once and retain Behavior resources." (let* ((palette (etaf-ref '(:fg "#111111" :bg "#ffffff"))) (installs 0) (cleanups 0) (calls nil) (behavior (etaf-behavior-create 'theme-layout :on-press (lambda () (push 'behavior calls)) :install (lambda () (cl-incf installs) (lambda () (cl-incf cleanups))))) (captured 'original) (on-press (let ((value captured)) (lambda () (push value calls))))) (etaf-dynamic-test--with-runtime (etaf-view (etaf-dynamic-test-theme-layout :palette palette :behavior behavior :on-press on-press)) (setq captured 'changed) (dolist (next '((:fg "#eeeeee" :bg "#111111") (:fg "#111111" :bg "#ffffff"))) (setf (etaf-value palette) next) (setq calls nil) (etaf-dispatch-event runtime 'theme-layout-first 'press) (should (equal calls '(behavior original))) (should (= installs 1)) (should (= cleanups 0)) (let ((props (etaf-runtime-host-props-for runtime 'theme-layout-first))) (should (eq (plist-get props :role) 'button)) (should-not (plist-get props :disabled)))) (cl-letf (((symbol-function 'etaf--runtime-swap-generation) (lambda (&rest _) (error "Reject Theme candidate")))) (should-error (setf (etaf-value palette) '(:fg "#eeeeee" :bg "#111111")))) (setq calls nil) (etaf-dispatch-event runtime 'theme-layout-first 'press) (should (equal calls '(behavior original))) (should (= installs 1)) (should (= cleanups 0))) (should (= cleanups 1)))) (provide 'etaf-dynamic-components-tests) ;;; etaf-dynamic-components-tests.el ends here