refactor: retain canonical Ebox inputs across Runtime commits

This commit is contained in:
Kinneyzhang 2026-08-28 16:48:24 +08:00
parent 0b254c1ff2
commit 0923925f1e
3 changed files with 871 additions and 530 deletions

View File

@ -72,6 +72,28 @@
(defvar etaf--rendering-range-p nil (defvar etaf--rendering-range-p nil
"Non-nil while eagerly lowering descendants of one Range item Host.") "Non-nil while eagerly lowering descendants of one Range item Host.")
(defvar etaf--ebox-source-builder nil
"Source builder owned by the current ETAF lowering boundary.")
(defun etaf--ebox-import-input (input)
"Import canonical INPUT into the current lowering and return its nodes."
(unless etaf--ebox-source-builder
(signal 'etaf-renderer-error
(list "Canonical Ebox input escaped its lowering boundary")))
(ebox-source-builder-import
etaf--ebox-source-builder
(ebox-canonical-input--source-index input))
(copy-sequence (ebox-canonical-input--nodes input)))
(defun etaf--ebox-input-for-nodes (nodes)
"Snapshot current source facts for canonical forest NODES."
(unless etaf--ebox-source-builder
(signal 'etaf-renderer-error
(list "Canonical Ebox nodes have no lowering source builder")))
(ebox-canonical-input-create
nodes
(ebox-tree-source-builder-snapshot etaf--ebox-source-builder nodes)))
(defun etaf--event-property-p (property) (defun etaf--event-property-p (property)
"Return non-nil when PROPERTY is an ETAF event callback property." "Return non-nil when PROPERTY is an ETAF event callback property."
(and (keywordp property) (and (keywordp property)
@ -81,7 +103,12 @@
"Return PROPS' explicit Host reference or one generated for PATH. "Return PROPS' explicit Host reference or one generated for PATH.
SITE-TOKEN replaces PATH as the generated call-site identity when non-nil." SITE-TOKEN replaces PATH as the generated call-site identity when non-nil."
(or (plist-get props :ref) (or (plist-get props :ref)
(list 'etaf-host (or site-token (copy-sequence path))))) (let ((site (or site-token (copy-sequence path))))
(list 'etaf-host
(if etaf--rendering-range-p
(list :range etaf--current-semantic-parent-id
:site site)
site)))))
(defun etaf--merge-property (props key value) (defun etaf--merge-property (props key value)
"Return PROPS with KEY set to VALUE, preserving the original order." "Return PROPS with KEY set to VALUE, preserving the original order."
@ -358,13 +385,13 @@ SITE-TOKEN supplies the stable generated Host identity when non-nil."
(setq ebox-props (nreverse ebox-props)) (setq ebox-props (nreverse ebox-props))
(setq ebox-props (setq ebox-props
(etaf--merge-property (etaf--merge-property
ebox-props :source-handle ebox-props :source-identity
(etaf--generated-host-ref source-props path site-token))) (etaf--generated-host-ref source-props path site-token)))
ebox-props)) ebox-props))
(defconst etaf--ebox-source-fields (defconst etaf--ebox-source-fields
'(:source-handle :key :class :id) '(:source-identity :key :class :id)
"Canonical Ebox source metadata forwarded by ETAF Renderer.") "ETAF source facts compiled into one opaque Ebox source handle.")
(defvar etaf--ebox-declaration-cache (make-hash-table :test #'equal) (defvar etaf--ebox-declaration-cache (make-hash-table :test #'equal)
"Bounded canonical declaration cache for normalized ETAF author props.") "Bounded canonical declaration cache for normalized ETAF author props.")
@ -436,37 +463,63 @@ SITE-TOKEN supplies the stable generated Host identity when non-nil."
(unless (stringp value) (unless (stringp value)
(signal 'etaf-renderer-error (signal 'etaf-renderer-error
(list (format "Text payload must resolve to a string: %S" value)))) (list (format "Text payload must resolve to a string: %S" value))))
(let ((declarations (etaf--ebox-declarations 'text props))) (unless (ebox-source-builder-p etaf--ebox-source-builder)
(apply #'ebox-text-create (signal 'etaf-renderer-error
(append (list :value value :declarations declarations) (list "ETAF Text lowering requires one source builder")))
(etaf--ebox-keep-properties (let* ((declarations (etaf--ebox-declarations 'text props))
props etaf--ebox-source-fields))))) (source-handle
(ebox-source-builder-bind
etaf--ebox-source-builder
:identity (plist-get props :source-identity)
:key (plist-get props :key)
:id (plist-get props :id)
:class (plist-get props :class)
:declarations declarations
:provenance '(:adapter etaf-renderer :tag text))))
(ebox-text-create
:value value
:owned-facts
(ebox-canonical-facts-from-declarations 'text declarations)
:source-handle source-handle)))
(defun etaf--ebox-box-node (tag props children) (defun etaf--ebox-box-node (tag props children)
"Return one typed Ebox BoxNode TAG with PROPS over canonical CHILDREN." "Return one typed Ebox BoxNode TAG with PROPS over canonical CHILDREN."
(unless (ebox-source-builder-p etaf--ebox-source-builder)
(signal 'etaf-renderer-error
(list "ETAF Box lowering requires one source builder")))
(let* ((declarations (etaf--ebox-declarations tag props)) (let* ((declarations (etaf--ebox-declarations tag props))
(layout (etaf--ebox-layout-config tag props declarations)) (layout (etaf--ebox-layout-config tag props declarations))
(outer (if (plist-member props :outer) (outer (if (plist-member props :outer)
(plist-get props :outer) (plist-get props :outer)
'block))) 'block))
(apply #'ebox-box-create (source-handle
(append (ebox-source-builder-bind
(list :layout layout :outer outer :children children etaf--ebox-source-builder
:declarations declarations) :identity (plist-get props :source-identity)
(etaf--ebox-keep-properties props etaf--ebox-source-fields))))) :key (plist-get props :key)
:id (plist-get props :id)
:class (plist-get props :class)
:declarations declarations
:provenance (list :adapter 'etaf-renderer :tag tag))))
(let ((ebox-canonical--source-builder etaf--ebox-source-builder))
(ebox-box-create
:layout layout :outer outer :children children
:owned-facts (ebox-canonical-facts-from-declarations tag declarations)
:source-handle source-handle))))
(defun etaf--ebox-forest-root (nodes source-handle) (defun etaf--ebox-forest-root (nodes source-identity)
"Return one canonical backend root for ordered forest NODES. "Return one canonical backend root for ordered forest NODES.
SOURCE-HANDLE identifies only a backend root introduced for an empty or SOURCE-IDENTITY belongs only to a backend root introduced for an empty or
multi-root forest; a single material root is returned unchanged." multi-root forest; a single material root is returned unchanged."
(cond (cond
((null nodes) ((null nodes)
(etaf--ebox-box-node (etaf--ebox-box-node
'box (list :source-handle source-handle) nil)) 'box (list :source-identity source-identity) nil))
((null (cdr nodes)) (car nodes)) ((null (cdr nodes))
(car nodes))
(t (t
(etaf--ebox-box-node (etaf--ebox-box-node
'column (list :source-handle source-handle) nodes)))) 'column (list :source-identity source-identity) nodes))))
(defun etaf--flatten-view-value (value) (defun etaf--flatten-view-value (value)
"Flatten VALUE through transparent `expr' and sequence values." "Flatten VALUE through transparent `expr' and sequence values."
@ -579,10 +632,19 @@ multi-root forest; a single material root is returned unchanged."
(if etaf--render-runtime (if etaf--render-runtime
(let ((result (etaf--runtime-render-component (let ((result (etaf--runtime-render-component
etaf--render-runtime item item-path))) etaf--render-runtime item item-path)))
(if (and (consp result) (cond
(eq (car result) 'component-output-range)) ((eq (car-safe result) 'component-output-material)
(cdr result) (etaf--ebox-import-input (cdr result)))
(list result))) ((eq (car-safe result) 'component-output-range)
(etaf--ebox-import-input (cdr result)))
((eq (car-safe result) 'component-output-anchor)
(list
(apply #'ebox-child-range
(nth 1 result)
(etaf--ebox-import-input (nth 2 result)))))
(t
(signal 'etaf-renderer-error
(list "Runtime returned invalid Component output")))))
(etaf--render-component-call-pure item item-path))) (etaf--render-component-call-pure item item-path)))
((etaf--slot-projection-p item) ((etaf--slot-projection-p item)
(etaf--render-slot-projection item item-path)) (etaf--render-slot-projection item item-path))
@ -744,12 +806,17 @@ RANGE-CHILD-P preserves the direct material Range parent."
;;;###autoload ;;;###autoload
(defun etaf-render (view) (defun etaf-render (view)
"Lower normalized VIEW to one Ebox node. "Lower normalized VIEW to one atomic canonical Ebox input.
This pure entry supports stateless Components. Stateful Components require a This pure entry supports stateless Components. Stateful Components require a
Runtime because their setup Scope must have a lifecycle owner." Runtime because their setup Scope must have a lifecycle owner."
(etaf--ebox-forest-root (let ((builder (ebox-source-builder-create)))
(etaf--render-value-list view '(root)) '(etaf-root pure))) (let* ((etaf--ebox-source-builder builder)
(root
(etaf--ebox-forest-root
(etaf--render-value-list view '(root)) '(etaf-root pure))))
(ebox-canonical-input-create
(list root) (ebox-source-builder-finish builder)))))
;;;###autoload ;;;###autoload
(defun etaf-mount (buffer-or-name view &optional options) (defun etaf-mount (buffer-or-name view &optional options)

File diff suppressed because it is too large Load Diff

View File

@ -96,6 +96,17 @@
(defvar etaf-test-detached-theme-source nil) (defvar etaf-test-detached-theme-source nil)
(defvar etaf-test-detached-row-renders 0) (defvar etaf-test-detached-row-renders 0)
(defun etaf-test--input-root (input)
"Return INPUT's single canonical root for structural assertions."
(ebox-canonical-input--single-root input "ETAF test input"))
(defun etaf-test--mounted-specified-value (buffer-name node name)
"Return mounted NODE's specified NAME from BUFFER-NAME's source generation."
(ebox-style-node-specified-value
node name nil
(plist-get (ebox--buffer-render-state (get-buffer buffer-name))
:source-index)))
(defun etaf-test--range-items () (defun etaf-test--range-items ()
"Return keyed text Views from `etaf-test-range-source'." "Return keyed text Views from `etaf-test-range-source'."
(cl-incf etaf-test-range-evals) (cl-incf etaf-test-range-evals)
@ -1284,8 +1295,10 @@
(column :outer 'block (column :outer 'block
(text "A") (text "A")
(text "B")))))) (text "B"))))))
(should (equal (ebox--computed-display row) '(inline row))) (should (equal (ebox--computed-display (etaf-test--input-root row))
(should (equal (ebox--computed-display column) '(block column))) '(inline row)))
(should (equal (ebox--computed-display (etaf-test--input-root column))
'(block column)))
(should (equal (substring-no-properties (ebox-render row)) "AB")) (should (equal (substring-no-properties (ebox-render row)) "AB"))
(should (equal (substring-no-properties (ebox-render column)) "A\nB")))) (should (equal (substring-no-properties (ebox-render column)) "A\nB"))))
@ -1323,8 +1336,9 @@
:grid-template-columns '((20) (20)) :grid-template-columns '((20) (20))
(text "A") (text "A")
(text "B")))))) (text "B"))))))
(should (ebox-box-node-p node)) (should (ebox-box-node-p (etaf-test--input-root node)))
(should (eq (ebox-layout-config-kind (ebox-box-node-layout node)) (should (eq (ebox-layout-config-kind
(ebox-box-node-layout (etaf-test--input-root node)))
'grid)) 'grid))
(should (string-match-p "A" (substring-no-properties (ebox-render node)))) (should (string-match-p "A" (substring-no-properties (ebox-render node))))
(should (string-match-p "B" (substring-no-properties (ebox-render node)))))) (should (string-match-p "B" (substring-no-properties (ebox-render node))))))
@ -1428,17 +1442,23 @@
(ert-deftest etaf-styles-have-root-class-and-inline-precedence () (ert-deftest etaf-styles-have-root-class-and-inline-precedence ()
"Apply component styles only at matching scope and preserve inline props." "Apply component styles only at matching scope and preserve inline props."
(let* ((node (etaf-render (etaf-view (etaf-test-styled-card)))) (let* ((input (etaf-render (etaf-view (etaf-test-styled-card))))
(source-index (ebox-canonical-input--source-index input))
(node (etaf-test--input-root input))
(children (ebox-box-node-children node)) (children (ebox-box-node-children node))
(title (car children)) (title (car children))
(body (cadr children))) (body (cadr children)))
(should (equal "inline-title" (should (equal "inline-title"
(ebox-style-node-specified-value title :color))) (ebox-style-node-specified-value
title :color nil source-index)))
(should (equal "title-bg" (should (equal "title-bg"
(ebox-style-node-specified-value title :bgcolor))) (ebox-style-node-specified-value
title :bgcolor nil source-index)))
(should (equal "style-root" (should (equal "style-root"
(ebox-style-node-specified-value node :color))) (ebox-style-node-specified-value
(should-not (ebox-style-node-specified-value body :color)))) node :color nil source-index)))
(should-not (ebox-style-node-specified-value
body :color nil source-index))))
(ert-deftest etaf-nil-host-props-allow-component-styles () (ert-deftest etaf-nil-host-props-allow-component-styles ()
"Treat an explicit nil Host style property as unspecified." "Treat an explicit nil Host style property as unspecified."
@ -1485,7 +1505,10 @@
(ebox-style-node-specified-value (ebox-style-node-specified-value
(etaf-runtime-root-node (etaf-runtime-root-node
(etaf-runtime-for-buffer buffer-name)) (etaf-runtime-for-buffer buffer-name))
:color)))) :color nil
(plist-get
(ebox--buffer-render-state (get-buffer buffer-name))
:source-index)))))
(when-let* ((runtime (etaf-runtime-for-buffer buffer-name))) (when-let* ((runtime (etaf-runtime-for-buffer buffer-name)))
(etaf-unmount runtime)) (etaf-unmount runtime))
(when-let* ((buffer (get-buffer buffer-name))) (when-let* ((buffer (get-buffer buffer-name)))
@ -1499,7 +1522,8 @@
(etaf-mount buffer-name (etaf-view (etaf-test-styled-parent))) (etaf-mount buffer-name (etaf-view (etaf-test-styled-parent)))
(should (should
(null (null
(ebox-style-node-specified-value (etaf-test--mounted-specified-value
buffer-name
(etaf-runtime-root-node (etaf-runtime-root-node
(etaf-runtime-for-buffer buffer-name)) (etaf-runtime-for-buffer buffer-name))
:color)))) :color))))
@ -1517,10 +1541,11 @@
(let* ((runtime (etaf-runtime-for-buffer buffer-name)) (let* ((runtime (etaf-runtime-for-buffer buffer-name))
(node (etaf-runtime-root-node runtime))) (node (etaf-runtime-root-node runtime)))
(should (equal "theme-color" (should (equal "theme-color"
(ebox-style-node-specified-value node :color))) (etaf-test--mounted-specified-value
buffer-name node :color)))
(should (equal "theme-bg" (should (equal "theme-bg"
(ebox-style-node-specified-value (etaf-test--mounted-specified-value
node :bgcolor))))) buffer-name node :bgcolor)))))
(when-let* ((runtime (etaf-runtime-for-buffer buffer-name))) (when-let* ((runtime (etaf-runtime-for-buffer buffer-name)))
(etaf-unmount runtime)) (etaf-unmount runtime))
(when-let* ((buffer (get-buffer buffer-name))) (when-let* ((buffer (get-buffer buffer-name)))
@ -1534,7 +1559,8 @@
(etaf-mount buffer-name (etaf-mount buffer-name
(etaf-view (etaf-test-themed-style-token))) (etaf-view (etaf-test-themed-style-token)))
(should (equal "token-color" (should (equal "token-color"
(ebox-style-node-specified-value (etaf-test--mounted-specified-value
buffer-name
(etaf-runtime-root-node (etaf-runtime-root-node
(etaf-runtime-for-buffer buffer-name)) (etaf-runtime-for-buffer buffer-name))
:color)))) :color))))
@ -1631,20 +1657,18 @@
(cl-labels (cl-labels
((ebox-host-node ((ebox-host-node
(host-ref) (host-ref)
(let* ((buffer (get-buffer buffer-name)) (ebox--host-ref-node (get-buffer buffer-name) host-ref))
(state (ebox--buffer-render-state buffer))
(node-id
(gethash host-ref
(plist-get state :host-ref-table))))
(gethash node-id (plist-get state :node-table))))
(background (background
(value) (value)
(if (tp-paint-slot-p value) (if (tp-paint-slot-p value)
(plist-get (tp-paint-slot-spec value) :background) (plist-get (tp-paint-slot-spec value) :background)
value))) value)))
(let ((slot (let ((slot
(ebox-style-node-specified-value (let* ((buffer (get-buffer buffer-name))
(ebox-host-node 'theme-atomic-panel) :bgcolor))) (state (ebox--buffer-render-state buffer)))
(ebox-style-node-specified-value
(ebox-host-node 'theme-atomic-panel) :bgcolor nil
(plist-get state :source-index)))))
(should (equal "#FFFFFF" (background slot))) (should (equal "#FFFFFF" (background slot)))
(etaf-dispatch-event runtime 'theme-atomic-toggle 'press) (etaf-dispatch-event runtime 'theme-atomic-toggle 'press)
(let ((semantic (let ((semantic
@ -2994,8 +3018,8 @@ Event composition is a Runtime contract, not a UI-library helper contract."
(etaf--component-instance-p value))) (etaf--component-instance-p value)))
('artifact ('artifact
(and (consp key) (integerp (car key)) (and (consp key) (integerp (car key))
(integerp (cdr key)) (listp value) (integerp (cdr key))
(plist-member value :node))) (ebox-canonical-input-p value)))
('route (etaf-runtime-route-p key))) ('route (etaf-runtime-route-p key)))
(error "injected %S journal write" phase)) (error "injected %S journal write" phase))
result)))) result))))
@ -3012,6 +3036,51 @@ Event composition is a Runtime contract, not a UI-library helper contract."
(etaf-unmount (etaf-runtime-for-buffer buffer-name)) (etaf-unmount (etaf-runtime-for-buffer buffer-name))
(when-let* ((buffer (get-buffer buffer-name))) (kill-buffer buffer))))) (when-let* ((buffer (get-buffer buffer-name))) (kill-buffer buffer)))))
(ert-deftest etaf-runtime-artifact-removal-failure-restores-retained-input ()
"Rollback an obsolete-artifact removal inside the prepublication journal."
(let ((buffer-name " *etaf-artifact-removal-rollback*")
(source (etaf-ref 0))
(etaf-test-retained-render-counts (make-hash-table :test #'eql)))
(unwind-protect
(progn
(etaf-mount buffer-name
(etaf--view-call 'etaf-test-retained-leaf
(list :label 1 :cell source) nil))
(let* ((runtime (etaf-runtime-for-buffer buffer-name))
(generation (etaf-runtime-current-generation runtime))
(registry (etaf-runtime-artifact-registry runtime))
old-key old-input
(before (etaf-test--buffer-text buffer-name))
(original-remhash (symbol-function 'remhash))
injected-p)
(maphash (lambda (key value)
(unless old-key
(setq old-key key old-input value)))
registry)
(should old-key)
(should (ebox-canonical-input-p old-input))
(cl-letf (((symbol-function 'remhash)
(lambda (key table)
(if (and (not injected-p)
(eq table registry)
(equal key old-key))
(progn
(setq injected-p t)
(funcall original-remhash key table)
(error "injected artifact removal failure"))
(funcall original-remhash key table)))))
(should-error (setf (etaf-value source) 1) :type 'error))
(should injected-p)
(should (eq generation (etaf-runtime-current-generation runtime)))
(should (eq old-input (gethash old-key registry)))
(should (equal before (etaf-test--buffer-text buffer-name)))
(etaf-runtime-flush runtime)
(should-not (equal before (etaf-test--buffer-text buffer-name)))))
(when-let* ((runtime (etaf-runtime-for-buffer buffer-name)))
(etaf-unmount runtime))
(when-let* ((buffer (get-buffer buffer-name)))
(kill-buffer buffer)))))
(ert-deftest etaf-runtime-route-arm-kth-failure-is-bounded-and-retryable () (ert-deftest etaf-runtime-route-arm-kth-failure-is-bounded-and-retryable ()
"Rollback each partial route arm and keep repeated failures bounded." "Rollback each partial route arm and keep repeated failures bounded."
(dolist (failure-index '(1 2)) (dolist (failure-index '(1 2))
@ -3297,6 +3366,7 @@ Event composition is a Runtime contract, not a UI-library helper contract."
child-id))))) child-id)))))
(dolist (items (list '((a . "A")) (dolist (items (list '((a . "A"))
'((a . "A") (b . "B")) '((a . "A") (b . "B"))
'((a . "A2") (b . "B"))
'((b . "B2")) nil)) '((b . "B2")) nil))
(setq etaf-test-range-evals 0 (setq etaf-test-range-evals 0
etaf-test-range-component-renders 0) etaf-test-range-component-renders 0)
@ -3361,6 +3431,28 @@ Event composition is a Runtime contract, not a UI-library helper contract."
(etaf-unmount runtime)) (etaf-unmount runtime))
(when-let* ((buffer (get-buffer buffer-name))) (kill-buffer buffer)))))) (when-let* ((buffer (get-buffer buffer-name))) (kill-buffer buffer))))))
(ert-deftest etaf-runtime-detached-range-removals-are-not-rescanned ()
"A Range's complete removal set should skip duplicate subtree traversal."
(let* ((candidate-children (make-hash-table :test #'eql))
(runtime
(etaf--runtime-create
:candidate-graph-children candidate-children
:candidate-graph-nodes (make-hash-table :test #'eql)
:candidate-removed-semantic-ids '(20 30)))
queries)
(puthash 10 nil candidate-children)
(cl-letf (((symbol-function 'etaf--generation-child-ids)
(lambda (_generation semantic-id)
(push semantic-id queries)
(if (= semantic-id 10)
'(20)
(error "Pre-recorded subtree was traversed: %S"
semantic-id)))))
(etaf--runtime-record-detached-candidate-subtrees runtime 'base))
(should (equal '(10) (nreverse queries)))
(should (equal '(20 30)
(etaf-runtime-candidate-removed-semantic-ids runtime)))))
(ert-deftest etaf-runtime-two-direct-ranges-batch-one-publication () (ert-deftest etaf-runtime-two-direct-ranges-batch-one-publication ()
"Evaluate and splice two disjoint Ranges once in one logical commit." "Evaluate and splice two disjoint Ranges once in one logical commit."
(let ((buffer-name " *etaf-two-range-test*") (let ((buffer-name " *etaf-two-range-test*")
@ -4196,8 +4288,12 @@ Event composition is a Runtime contract, not a UI-library helper contract."
(should (eq 'transparent (should (eq 'transparent
(etaf--semantic-component-publication-kind component))) (etaf--semantic-component-publication-kind component)))
(should (eq 'component-output (etaf--semantic-range-kind range))) (should (eq 'component-output (etaf--semantic-range-kind range)))
(should (eq (etaf--semantic-component-output-range-ref component) (should-not (etaf--semantic-component-artifact-key component))
(etaf--semantic-range-range-ref range))) (should (etaf--semantic-range-range-ref range))
(should
(ebox-canonical-input-p
(gethash (etaf--semantic-range-artifact-key range)
(etaf-runtime-range-artifact-registry runtime))))
(let ((committed generation) (let ((committed generation)
(before (etaf-test--buffer-text buffer-name))) (before (etaf-test--buffer-text buffer-name)))
(cl-letf (((symbol-function 'accept-change-group) (cl-letf (((symbol-function 'accept-change-group)
@ -4804,4 +4900,43 @@ Event composition is a Runtime contract, not a UI-library helper contract."
(when-let* ((buffer (get-buffer buffer-name))) (when-let* ((buffer (get-buffer buffer-name)))
(kill-buffer buffer))))) (kill-buffer buffer)))))
(ert-deftest etaf-runtime-publishes-handle-only-ebox-sources ()
"Renderer should publish one source index and handle-only Ebox nodes."
(let ((buffer-name " *etaf-source-index-boundary*"))
(unwind-protect
(progn
(etaf-mount
buffer-name
(etaf-view
(column :id "app" :class "shell"
(box :key 'row :id "row" :class "entry" "A"))))
(let* ((buffer (get-buffer buffer-name))
(state (ebox--buffer-render-state buffer))
(source-index (plist-get state :source-index))
(match (car (ebox-selector-query-buffer buffer "#row")))
(node (plist-get match :node))
(record
(ebox-source-index-record
source-index (ebox-node-source-handle node))))
(should (ebox-source-index-p source-index))
(should (equal '("entry")
(ebox-source-record-classes record)))
(should (eq 'row (ebox-source-record-key record)))
(maphash
(lambda (_node-id candidate-node)
(when (ebox-node-kind candidate-node)
(should
(ebox-source-index-record
source-index
(ebox-node-source-handle candidate-node)))
(dolist (field
'(:key :id :class :host-ref
:ebox-style-declarations))
(should-not (plist-member candidate-node field)))))
(plist-get state :node-table))))
(when-let* ((runtime (etaf-runtime-for-buffer buffer-name)))
(etaf-unmount runtime))
(when-let* ((buffer (get-buffer buffer-name)))
(kill-buffer buffer)))))
;;; etaf-tests.el ends here ;;; etaf-tests.el ends here