2237 lines
105 KiB
EmacsLisp
2237 lines
105 KiB
EmacsLisp
;;; ebox-commit-tests.el --- Declarative commit smoke tests -*- lexical-binding: t; -*-
|
|
|
|
(require 'cl-lib)
|
|
(require 'ert)
|
|
(require 'ebox)
|
|
(require 'ebox-native-commit)
|
|
|
|
;; These tests lock the named Elisp projection proofs. Native commit has its
|
|
;; own focused contract tests below; disable runtime module discovery here so
|
|
;; a locally built optional module cannot silently replace paint/span/mixed
|
|
;; plans with `native-frame' and make this suite environment-dependent.
|
|
(setq ebox-native-reflow-module-path nil)
|
|
|
|
(defun ebox-commit-test--buffer-string (buffer)
|
|
"Return BUFFER's complete propertized contents."
|
|
(with-current-buffer buffer
|
|
(save-restriction
|
|
(widen)
|
|
(buffer-substring (point-min) (point-max)))))
|
|
|
|
(ert-deftest ebox-native-fragment-style-delta-copies-only-changed-records ()
|
|
"A native style delta keeps the retained template immutable."
|
|
(let* ((first [0 1 0 nil nil nil nil (1)])
|
|
(second [1 2 0 nil nil nil nil (2)])
|
|
(template (vector first second))
|
|
(target
|
|
(ebox-native-commit--apply-fragment-style-delta
|
|
template '((1 3 5)))))
|
|
(should (eq (aref target 0) first))
|
|
(should-not (eq (aref target 1) second))
|
|
(should (equal (aref (aref target 1) 7) '(3 5)))
|
|
(should (equal (aref second 7) '(2)))))
|
|
|
|
(ert-deftest ebox-native-object-delta-orders-moved-and-new-nodes ()
|
|
"A topology delta names parents before moved and introduced children."
|
|
(let* ((old-root
|
|
(ebox-column (ebox-create :key 'a :content "A")
|
|
(ebox-create :key 'b :content "B")))
|
|
(_old-ids (ebox--runtime-node-ids old-root))
|
|
(old-index (ebox--runtime-index old-root t))
|
|
(old-objects (make-hash-table :test 'equal))
|
|
(new-root
|
|
(ebox-column (ebox-create :key 'b :content "B")
|
|
(ebox-create :key 'a :content "A")
|
|
(ebox-create :key 'c :content "C")))
|
|
(_reconciled (ebox-tree-reconcile-runtime old-root new-root))
|
|
(new-index (ebox--runtime-index new-root t))
|
|
(root-id (plist-get old-root :node-id)))
|
|
(maphash (lambda (node-id _node)
|
|
(puthash node-id (list 'object node-id) old-objects))
|
|
(plist-get old-index :node-table))
|
|
(let* ((old-state (append (list :root-node old-root
|
|
:surface-node-object-table old-objects)
|
|
old-index))
|
|
(new-state (append (list :root-node new-root) new-index))
|
|
(delta
|
|
(ebox-native-commit-object-delta-node-ids
|
|
old-state new-state
|
|
(list :touched-node-ids (list root-id)
|
|
:removed-node-ids nil)))
|
|
(children (ebox-tree--children-raw new-root)))
|
|
(should
|
|
(equal delta
|
|
(cons root-id (mapcar (lambda (node)
|
|
(plist-get node :node-id))
|
|
children)))))))
|
|
|
|
(ert-deftest ebox-native-object-delta-requires-complete-removal-proof ()
|
|
"An unreported disappeared object rejects the native topology delta."
|
|
(let* ((old-root
|
|
(ebox-column (ebox-create :key 'a :content "A")
|
|
(ebox-create :key 'tail :content "T")
|
|
(ebox-create :key 'b :content "B")))
|
|
(_old-ids (ebox--runtime-node-ids old-root))
|
|
(old-index (ebox--runtime-index old-root t))
|
|
(old-objects (make-hash-table :test 'equal))
|
|
(removed-node (car (last (ebox-tree--children-raw old-root))))
|
|
(new-root
|
|
(ebox-column (ebox-create :key 'a :content "A")
|
|
(ebox-create :key 'tail :content "T")))
|
|
(_reconciled (ebox-tree-reconcile-runtime old-root new-root))
|
|
(new-index (ebox--runtime-index new-root t))
|
|
(root-id (plist-get old-root :node-id)))
|
|
(maphash (lambda (node-id _node)
|
|
(puthash node-id (list 'object node-id) old-objects))
|
|
(plist-get old-index :node-table))
|
|
(let ((old-state (append (list :root-node old-root
|
|
:surface-node-object-table old-objects)
|
|
old-index))
|
|
(new-state (append (list :root-node new-root) new-index)))
|
|
(should-not
|
|
(ebox-native-commit-object-delta-node-ids
|
|
old-state new-state
|
|
(list :touched-node-ids (list root-id)
|
|
:removed-node-ids nil)))
|
|
(should
|
|
(equal
|
|
(list root-id)
|
|
(ebox-native-commit-object-delta-node-ids
|
|
old-state new-state
|
|
(list :touched-node-ids (list root-id)
|
|
:removed-node-ids
|
|
(list (plist-get removed-node :node-id)))))))))
|
|
|
|
(ert-deftest ebox-style-schema-registration-is-not-per-node-copy ()
|
|
"Repeated node construction must not copy the whole ECSS schema domain."
|
|
(let ((calls 0)
|
|
(original (symbol-function 'ecss-schema-set-property)))
|
|
(cl-letf (((symbol-function 'ecss-schema-set-property)
|
|
(lambda (&rest arguments)
|
|
(cl-incf calls)
|
|
(apply original arguments))))
|
|
(dotimes (_ 24)
|
|
(ebox-create :content "schema-hot-path" :color "#111111")))
|
|
(should (= calls 0))))
|
|
|
|
(ert-deftest ebox-style-declaration-compilation-is-memoized ()
|
|
"Repeated equivalent style declarations compile through ECSS once."
|
|
(clrhash ebox-style--declaration-cache)
|
|
(let ((calls 0)
|
|
(original (symbol-function 'ecss-merge-declarations)))
|
|
(cl-letf (((symbol-function 'ecss-merge-declarations)
|
|
(lambda (&rest arguments)
|
|
(cl-incf calls)
|
|
(apply original arguments))))
|
|
(dotimes (_ 24)
|
|
(ebox-style-compile-declarations
|
|
'(:color "#111111" :bgcolor "#222222"))))
|
|
(should (= calls 1))))
|
|
|
|
(ert-deftest ebox-commit-publishes-content-change ()
|
|
"A declarative commit should publish changed content."
|
|
(let* ((buffer (ebox-render-to-buffer
|
|
(generate-new-buffer-name " *ebox-commit-content*")
|
|
(ebox-create :key 'root :content "Before" :width '(80))))
|
|
(report (ebox-commit
|
|
buffer
|
|
(ebox-create :key 'root :content "After" :width '(80)))))
|
|
(unwind-protect
|
|
(progn
|
|
(should (string-prefix-p
|
|
"After"
|
|
(string-trim-right
|
|
(substring-no-properties
|
|
(ebox-commit-test--buffer-string buffer)))))
|
|
(should (plist-get report :runtime-published))
|
|
(should (> (plist-get report :patch-count) 0)))
|
|
(when (buffer-live-p buffer)
|
|
(kill-buffer buffer)))))
|
|
|
|
(ert-deftest ebox-commit-preserves-keyed-sibling-identity ()
|
|
"Keyed siblings should remain addressable after a reorder commit."
|
|
(let* ((buffer (ebox-render-to-buffer
|
|
(generate-new-buffer-name " *ebox-commit-keyed*")
|
|
(ebox-column
|
|
(ebox-create :key 'a :host-ref 'a :content "A" :width '(40))
|
|
(ebox-create :key 'b :host-ref 'b :content "B" :width '(40)))))
|
|
(old-b (ebox-host-ref-position buffer 'b))
|
|
(report (ebox-commit
|
|
buffer
|
|
(ebox-column
|
|
(ebox-create :key 'b :host-ref 'b :content "B2" :width '(40))
|
|
(ebox-create :key 'a :host-ref 'a :content "A" :width '(40))))))
|
|
(unwind-protect
|
|
(progn
|
|
(should old-b)
|
|
(should (ebox-host-ref-position buffer 'b))
|
|
(should (plist-get report :runtime-published))
|
|
(should (string-match-p "B2"
|
|
(substring-no-properties
|
|
(ebox-commit-test--buffer-string buffer)))))
|
|
(when (buffer-live-p buffer)
|
|
(kill-buffer buffer)))))
|
|
|
|
(ert-deftest ebox-candidate-root-replacement-is-last-wins-and-absorbing ()
|
|
"The private root address absorbs descendant operations without ref overlap."
|
|
(let* ((buffer
|
|
(ebox-render-to-buffer
|
|
(generate-new-buffer-name " *ebox-root-candidate*")
|
|
(ebox-column
|
|
(ebox-create :key 'a :host-ref 'a :content "A" :width '(40))
|
|
(ebox-create :key 'b :host-ref 'b :content "B" :width '(40)))))
|
|
(surface (with-current-buffer buffer ebox-surface--buffer-surface))
|
|
(revision (tp-surface-revision surface))
|
|
(candidate (ebox-candidate-begin buffer)))
|
|
(unwind-protect
|
|
(progn
|
|
(ebox-candidate-replace-host-ref
|
|
candidate 'a (ebox-create :key 'a :content "ignored-before"))
|
|
(ebox-candidate-replace-root
|
|
candidate
|
|
(ebox-create :key 'root :host-ref 'a :content "first-root"
|
|
:width '(80)))
|
|
(ebox-candidate-replace-host-ref
|
|
candidate 'b (ebox-create :key 'b :content "ignored-after"))
|
|
(ebox-candidate-replace-root
|
|
candidate
|
|
(ebox-create :key 'root :host-ref 'b :content "final-root"
|
|
:width '(80)))
|
|
(should (= (length (ebox-candidate--replacements candidate)) 1))
|
|
(let ((report (ebox-commit buffer candidate)))
|
|
(should (string-match-p
|
|
"final-root"
|
|
(substring-no-properties
|
|
(ebox-commit-test--buffer-string buffer))))
|
|
(should-not (string-match-p
|
|
"ignored"
|
|
(substring-no-properties
|
|
(ebox-commit-test--buffer-string buffer))))
|
|
(should (= (tp-surface-revision surface) (1+ revision)))
|
|
(should (plist-get report :runtime-published)))
|
|
(should-error
|
|
(ebox-candidate-replace-root
|
|
candidate (ebox-create :key 'root :content "sealed"))))
|
|
(when (buffer-live-p buffer) (kill-buffer buffer)))))
|
|
|
|
(ert-deftest ebox-commit-framework-participant-completes-and-rolls-back ()
|
|
"Framework publication is paired, diagnosed, and completed exactly once."
|
|
(let ((buffer (generate-new-buffer " *ebox-framework-participant*")))
|
|
(unwind-protect
|
|
(progn
|
|
(ebox-render-to-buffer
|
|
buffer (ebox-create :key 'root :content "old" :width '(80)))
|
|
(let (trace)
|
|
(let ((report
|
|
(ebox-commit
|
|
buffer (ebox-create :key 'root :content "new" :width '(80))
|
|
(lambda (_report) (push 'publish trace))
|
|
(lambda (_report) (push 'rollback trace)))))
|
|
(should (equal trace '(publish)))
|
|
(should (eq (plist-get report :framework-participant-state)
|
|
'completed))
|
|
(should-not
|
|
(plist-get report :framework-participant-diagnostics))))
|
|
(let* ((before (ebox-commit-test--buffer-string buffer))
|
|
(original
|
|
(symbol-function 'tp--run-transaction-precommit-functions))
|
|
trace captured failure)
|
|
(cl-letf
|
|
(((symbol-function 'tp--run-transaction-precommit-functions)
|
|
(lambda ()
|
|
(funcall original)
|
|
(error "later TP failure"))))
|
|
(setq failure
|
|
(condition-case condition
|
|
(ebox-commit
|
|
buffer
|
|
(ebox-create :key 'root :content "rejected"
|
|
:width '(80))
|
|
(lambda (report)
|
|
(setq captured report)
|
|
(push 'publish trace))
|
|
(lambda (_report)
|
|
(push 'rollback trace)
|
|
(error "rollback diagnostic")))
|
|
(error condition))))
|
|
(should (equal trace '(rollback publish)))
|
|
(should (equal (cadr failure) "later TP failure"))
|
|
(should (equal-including-properties
|
|
(ebox-commit-test--buffer-string buffer) before))
|
|
(should (eq (plist-get captured :framework-participant-state)
|
|
'rolled-back))
|
|
(should (= (length
|
|
(plist-get
|
|
captured :framework-participant-diagnostics))
|
|
1))))
|
|
(when (buffer-live-p buffer) (kill-buffer buffer)))))
|
|
|
|
(ert-deftest ebox-commit-framework-publish-failure-rolls-back-full-and-scoped ()
|
|
"A framework publish failure invokes its pair once on both commit paths."
|
|
(dolist (mode '(full scoped))
|
|
(let ((buffer (generate-new-buffer " *ebox-framework-publish-fail*")))
|
|
(unwind-protect
|
|
(progn
|
|
(ebox-render-to-buffer
|
|
buffer
|
|
(ebox-column
|
|
(ebox-create :key 'a :host-ref 'a :content "old-a" :width '(40))
|
|
(ebox-create :key 'b :host-ref 'b :content "old-b" :width '(40))))
|
|
(let ((before (ebox-commit-test--buffer-string buffer))
|
|
(candidate (ebox-candidate-begin buffer))
|
|
trace captured)
|
|
(when (eq mode 'scoped)
|
|
(ebox-candidate-replace-host-ref
|
|
candidate 'a
|
|
(ebox-create :key 'a :host-ref 'a :content "new-a"
|
|
:width '(40))))
|
|
(should-error
|
|
(ebox-commit
|
|
buffer
|
|
(if (eq mode 'scoped)
|
|
candidate
|
|
(ebox-create :key 'root :content "new-root" :width '(80)))
|
|
(lambda (report)
|
|
(setq captured report)
|
|
(push 'publish trace)
|
|
(error "framework publish failed"))
|
|
(lambda (_report) (push 'rollback trace))))
|
|
(should (equal trace '(rollback publish)))
|
|
(should (eq (plist-get captured :framework-participant-state)
|
|
'rolled-back))
|
|
(should (equal-including-properties
|
|
(ebox-commit-test--buffer-string buffer) before))))
|
|
(when (buffer-live-p buffer) (kill-buffer buffer))))))
|
|
|
|
(ert-deftest ebox-commit-framework-argument-validation ()
|
|
"Four-argument framework callbacks have an exact paired contract."
|
|
(let ((buffer (generate-new-buffer " *ebox-framework-validation*"))
|
|
(root (ebox-create :key 'root :content "x")))
|
|
(unwind-protect
|
|
(progn
|
|
(ebox-render-to-buffer buffer root)
|
|
(should-error (ebox-commit buffer root 7) :type 'wrong-type-argument)
|
|
(should-error
|
|
(ebox-commit buffer root nil #'ignore))
|
|
(should-error
|
|
(ebox-commit buffer root #'ignore 7)
|
|
:type 'wrong-type-argument))
|
|
(when (buffer-live-p buffer) (kill-buffer buffer)))))
|
|
|
|
(ert-deftest ebox-commit-expands-scope-for-length-changing-column-content ()
|
|
"A column content growth must publish shifted later styled siblings."
|
|
(let* ((buffer (generate-new-buffer-name " *ebox-commit-column-scope*"))
|
|
(old-root
|
|
(ebox-column
|
|
(ebox-create :key 'panel :padding '(1 (2))
|
|
:border "#687386" :bgcolor "#FFFDF8"
|
|
:content "Panel")
|
|
(ebox-create :key 'payload :padding '(0 (1))
|
|
:border "#AAA" :content "No payload yet.")
|
|
(ebox-create :key 'later :padding '(0 (1))
|
|
:border "#BBB" :content "Later sibling")))
|
|
(new-root
|
|
(ebox-column
|
|
(ebox-create :key 'panel :padding '(1 (2))
|
|
:border "#687386" :bgcolor "#FFFDF8"
|
|
:content "Panel")
|
|
(ebox-create :key 'payload :padding '(0 (1))
|
|
:border "#AAA"
|
|
:content "Payload received: payload=42")
|
|
(ebox-create :key 'later :padding '(0 (1))
|
|
:border "#BBB" :content "Later sibling")))
|
|
(report nil))
|
|
(unwind-protect
|
|
(progn
|
|
(ebox-render-to-buffer buffer old-root)
|
|
(setq report (ebox-commit buffer new-root))
|
|
(should (string-match-p
|
|
"Payload received: payload=42"
|
|
(with-current-buffer buffer (buffer-string))))
|
|
(should (string-match-p "Later sibling"
|
|
(with-current-buffer buffer (buffer-string))))
|
|
(should (eq (plist-get report :strategy) 'owner-rerender))
|
|
(should (equal (plist-get report :patch-ops) '(owner-rerender)))
|
|
(should-not (plist-get report :tp-scope-fallback)))
|
|
(when (buffer-live-p buffer)
|
|
(kill-buffer buffer)))))
|
|
|
|
(ert-deftest ebox-commit-reuses-unchanged-style-computations ()
|
|
"A content-only commit should not recompute unchanged retained styles."
|
|
(let* ((ebox-style-stylesheet (ecss-stylesheet-create))
|
|
(calls 0)
|
|
(original (symbol-function 'ecss-compute-style))
|
|
(buffer nil))
|
|
(ebox-style-add-rule ".card" '(:color "#111111") :layer 'components)
|
|
(unwind-protect
|
|
(progn
|
|
(setq buffer
|
|
(ebox-render-to-buffer
|
|
(generate-new-buffer-name " *ebox-commit-style-cache*")
|
|
(ebox-column
|
|
(ebox-create :key 'first :class "card" :content "Before"
|
|
:width '(40))
|
|
(ebox-create :key 'second :class "card" :content "Stable"
|
|
:width '(40)))))
|
|
(setq calls 0)
|
|
(cl-letf (((symbol-function 'ecss-compute-style)
|
|
(lambda (&rest arguments)
|
|
(cl-incf calls)
|
|
(apply original arguments))))
|
|
(ebox-commit
|
|
buffer
|
|
(ebox-column
|
|
(ebox-create :key 'first :class "card" :content "After"
|
|
:width '(40))
|
|
(ebox-create :key 'second :class "card" :content "Stable"
|
|
:width '(40))))
|
|
(should (= calls 0))))
|
|
(when (buffer-live-p buffer)
|
|
(kill-buffer buffer)))))
|
|
|
|
(ert-deftest ebox-commit-builds-one-selector-tree-snapshot ()
|
|
"A styled commit should snapshot selector context once for the whole tree."
|
|
(let* ((ebox-style-stylesheet (ecss-stylesheet-create))
|
|
(calls 0)
|
|
(original (symbol-function 'ebox-surface--subject-signature))
|
|
(buffer nil))
|
|
(ebox-style-add-rule ".card" '(:color "#111111") :layer 'components)
|
|
(unwind-protect
|
|
(progn
|
|
(setq buffer
|
|
(ebox-render-to-buffer
|
|
(generate-new-buffer-name " *ebox-commit-selector-snapshot*")
|
|
(ebox-column
|
|
(ebox-create :key 'first :class "card" :content "Before"
|
|
:width '(40))
|
|
(ebox-create :key 'second :class "card" :content "Stable"
|
|
:width '(40)))))
|
|
(cl-letf (((symbol-function 'ebox-surface--subject-signature)
|
|
(lambda (&rest arguments)
|
|
(cl-incf calls)
|
|
(apply original arguments))))
|
|
(ebox-commit
|
|
buffer
|
|
(ebox-column
|
|
(ebox-create :key 'first :class "card" :content "After"
|
|
:width '(40))
|
|
(ebox-create :key 'second :class "card" :content "Stable"
|
|
:width '(40))))
|
|
(should (= calls 1))))
|
|
(when (buffer-live-p buffer)
|
|
(kill-buffer buffer)))))
|
|
|
|
(ert-deftest ebox-commit-reuses-local-selector-styles-across-tree-change ()
|
|
"A subject-local stylesheet should compute only newly inserted node styles."
|
|
(let* ((ebox-style-stylesheet (ecss-stylesheet-create))
|
|
(calls 0)
|
|
(original (symbol-function 'ecss-compute-style))
|
|
(buffer nil))
|
|
(ebox-style-add-rule ".card" '(:color "#2255AA") :layer 'components)
|
|
(unwind-protect
|
|
(progn
|
|
(setq buffer
|
|
(ebox-render-to-buffer
|
|
(generate-new-buffer-name " *ebox-local-selector-reuse*")
|
|
(ebox-column
|
|
(ebox-create :key 'first :class "card" :content "First"
|
|
:width '(40))
|
|
(ebox-create :key 'second :class "card" :content "Second"
|
|
:width '(40)))))
|
|
(cl-letf (((symbol-function 'ecss-compute-style)
|
|
(lambda (&rest arguments)
|
|
(cl-incf calls)
|
|
(apply original arguments))))
|
|
(ebox-commit
|
|
buffer
|
|
(ebox-column
|
|
(ebox-create :key 'first :class "card" :content "First"
|
|
:width '(40))
|
|
(ebox-create :key 'second :class "card" :content "Second"
|
|
:width '(40))
|
|
(ebox-create :key 'third :class "card" :content "Third"
|
|
:width '(40)))))
|
|
(should (= calls 1))
|
|
(let* ((text (ebox-commit-test--buffer-string buffer))
|
|
(position (string-match "Third" text)))
|
|
(should position)
|
|
(should (equal (plist-get (get-text-property position 'face text)
|
|
:foreground)
|
|
"#2255AA"))))
|
|
(when (buffer-live-p buffer)
|
|
(kill-buffer buffer)))))
|
|
|
|
(ert-deftest ebox-commit-reuses-successful-allocation-certificate ()
|
|
"A repeated fixed-slot update must not rescan unchanged ancestor spans."
|
|
(let* ((ebox-style-stylesheet (ecss-stylesheet-create))
|
|
(buffer nil)
|
|
(ancestor-scans 0))
|
|
(ebox-style-add-rule ".card" '(:color "#111111") :layer 'components)
|
|
(unwind-protect
|
|
(progn
|
|
(setq buffer
|
|
(ebox-render-to-buffer
|
|
(generate-new-buffer-name " *ebox-allocation-certificate*")
|
|
(ebox-build
|
|
'(grid :width (100) :height 2
|
|
:grid-template-columns (1fr 1fr)
|
|
(column :key card-shell :width stretch :height 2
|
|
(box :key card :class "card" :host-ref target
|
|
:content "Before" :width (40)))
|
|
(box :key stable :content "Stable"
|
|
:width (40) :height 1)))))
|
|
(let ((first (ebox-candidate-begin buffer)))
|
|
(ebox-candidate-replace-host-ref
|
|
first 'target
|
|
(ebox-create :key 'card :class "card" :host-ref 'target
|
|
:content "Middle" :width '(40)))
|
|
(should (eq (plist-get (ebox-commit buffer first)
|
|
:projection-kind)
|
|
'owner-scoped)))
|
|
(let* ((state (ebox--buffer-render-state buffer))
|
|
(certificates
|
|
(plist-get state :retained-allocation-certificates)))
|
|
(should (hash-table-p certificates))
|
|
(should (= 1 (hash-table-count certificates))))
|
|
(let ((second (ebox-candidate-begin buffer))
|
|
(original
|
|
(symbol-function
|
|
'ebox-incremental--compute-ancestor-slot-signature)))
|
|
(ebox-candidate-replace-host-ref
|
|
second 'target
|
|
(ebox-create :key 'card :class "card" :host-ref 'target
|
|
:content "After" :width '(40)))
|
|
(cl-letf
|
|
(((symbol-function
|
|
'ebox-incremental--compute-ancestor-slot-signature)
|
|
(lambda (&rest arguments)
|
|
(cl-incf ancestor-scans)
|
|
(apply original arguments))))
|
|
(should (eq (plist-get (ebox-commit buffer second)
|
|
:projection-kind)
|
|
'owner-scoped))))
|
|
(should (= ancestor-scans 0))
|
|
(should
|
|
(string-match-p
|
|
"After"
|
|
(substring-no-properties
|
|
(ebox-commit-test--buffer-string buffer)))))
|
|
(when (buffer-live-p buffer)
|
|
(kill-buffer buffer)))))
|
|
|
|
(ert-deftest ebox-commit-invalidates-selector-tree-token-for-sibling-change ()
|
|
"A sibling metadata change must invalidate retained selector computations."
|
|
(let* ((ebox-style-stylesheet (ecss-stylesheet-create))
|
|
(buffer nil))
|
|
(ebox-style-add-rule ".active + .target" '(:color "#2255AA")
|
|
:layer 'components)
|
|
(unwind-protect
|
|
(progn
|
|
(setq buffer
|
|
(ebox-render-to-buffer
|
|
(generate-new-buffer-name " *ebox-commit-selector-change*")
|
|
(ebox-column
|
|
(ebox-create :key 'state :class "inactive" :content "State"
|
|
:width '(40))
|
|
(ebox-create :key 'target :class "target" :content "Target"
|
|
:width '(40)))))
|
|
(let* ((before (ebox-commit-test--buffer-string buffer))
|
|
(position (string-match "Target" before)))
|
|
(should position)
|
|
(should-not (get-text-property position 'face before)))
|
|
(ebox-commit
|
|
buffer
|
|
(ebox-column
|
|
(ebox-create :key 'state :class "active" :content "State"
|
|
:width '(40))
|
|
(ebox-create :key 'target :class "target" :content "Target"
|
|
:width '(40))))
|
|
(let* ((after (ebox-commit-test--buffer-string buffer))
|
|
(position (string-match "Target" after)))
|
|
(should position)
|
|
(should (equal (plist-get (get-text-property position 'face after)
|
|
:foreground)
|
|
"#2255AA"))))
|
|
(when (buffer-live-p buffer)
|
|
(kill-buffer buffer)))))
|
|
|
|
(ert-deftest ebox-commit-observes-in-place-stylesheet-changes ()
|
|
"A retained commit must refresh when its stylesheet changes in place."
|
|
(let* ((ebox-style-stylesheet (ecss-stylesheet-create))
|
|
(buffer nil))
|
|
(ebox-style-add-rule ".card" '(:color "#111111") :layer 'components)
|
|
(unwind-protect
|
|
(progn
|
|
(setq buffer
|
|
(ebox-render-to-buffer
|
|
(generate-new-buffer-name " *ebox-commit-style-rule*")
|
|
(ebox-create :key 'card :class "card" :content "Stable"
|
|
:width '(40))))
|
|
(should (equal (plist-get (get-text-property
|
|
(point-min) 'face buffer) :foreground)
|
|
"#111111"))
|
|
(ebox-style-add-rule ".card" '(:color "#222222") :layer 'components)
|
|
(ebox-commit
|
|
buffer
|
|
(ebox-create :key 'card :class "card" :content "Stable"
|
|
:width '(40)))
|
|
(should (equal (plist-get (get-text-property
|
|
(point-min) 'face buffer) :foreground)
|
|
"#222222")))
|
|
(when (buffer-live-p buffer)
|
|
(kill-buffer buffer)))))
|
|
|
|
(ert-deftest ebox-commit-observes-cascade-activation-during-content-change ()
|
|
"A commit must not span-patch across an inactive-to-active cascade change."
|
|
(let* ((ebox-style-stylesheet (ecss-stylesheet-create))
|
|
(buffer nil))
|
|
(unwind-protect
|
|
(progn
|
|
(setq buffer
|
|
(ebox-render-to-buffer
|
|
(generate-new-buffer-name " *ebox-commit-cascade-activation*")
|
|
(ebox-create :key 'card :class "card" :content "Before"
|
|
:width '(40))))
|
|
(should-not (get-text-property (point-min) 'face buffer))
|
|
(ebox-style-add-rule ".card" '(:color "#2255AA")
|
|
:layer 'components)
|
|
(let ((report
|
|
(ebox-commit
|
|
buffer
|
|
(ebox-create :key 'card :class "card" :content "After"
|
|
:width '(40)))))
|
|
(should (string-prefix-p
|
|
"After"
|
|
(string-trim-right
|
|
(substring-no-properties
|
|
(ebox-commit-test--buffer-string buffer)))))
|
|
(should-not (eq (plist-get report :strategy) 'span-patch))
|
|
(should (equal (plist-get (get-text-property
|
|
(point-min) 'face buffer) :foreground)
|
|
"#2255AA"))))
|
|
(when (buffer-live-p buffer)
|
|
(kill-buffer buffer)))))
|
|
|
|
(ert-deftest ebox-commit-uses-static-cascade-owner-proof ()
|
|
"A static cascade content commit may plan its local owner."
|
|
(let* ((ebox-style-stylesheet (ecss-stylesheet-create))
|
|
(calls 0)
|
|
(ensured 0)
|
|
(rendered nil)
|
|
(buffer nil))
|
|
(ebox-style-add-rule ".card" '(:color "#111111") :layer 'components)
|
|
(unwind-protect
|
|
(progn
|
|
(setq buffer
|
|
(ebox-render-to-buffer
|
|
(generate-new-buffer-name " *ebox-commit-proof-cache*")
|
|
(ebox-build
|
|
'(column :width (100) :height 4
|
|
(column :key main :width (100) :height 2
|
|
(grid :key grid :width (100) :height 2
|
|
:grid-template-columns (1fr 1fr)
|
|
(column :key card-shell :width stretch :height 2
|
|
(box :key card :class "card" :host-ref target
|
|
:content "Before"))
|
|
(box :key stable :content "Stable"
|
|
:width (40) :height 1)))
|
|
(box :key footer :content "Footer"
|
|
:width (100) :height 1)))))
|
|
(let* ((state (ebox--buffer-render-state buffer))
|
|
(style-states (plist-get state :style-binding-states))
|
|
(binding-state
|
|
(cl-loop for value being the hash-values of style-states
|
|
when (and (plist-get value :declarations)
|
|
(plist-get value :parent-style-state)
|
|
(ebox-surface--static-style-state-p value))
|
|
return value)))
|
|
(should binding-state)
|
|
(should (tp-binding-live-p (plist-get binding-state :binding)))
|
|
(should (plist-get binding-state :parent-style-state))
|
|
(should (= 1 (plist-get binding-state
|
|
:inherited-dependency-count)))
|
|
(should (= 1 (tp-binding-dependency-count
|
|
(plist-get binding-state :binding))))
|
|
(should (ebox-surface--static-style-state-p binding-state)))
|
|
(let ((original
|
|
(symbol-function 'ebox-incremental--layout-owner-plan))
|
|
(original-ensure
|
|
(symbol-function 'ebox-surface--ensure-node-tree))
|
|
(original-render (symbol-function 'ebox--render-layout)))
|
|
(cl-letf (((symbol-function 'ebox-incremental--layout-owner-plan)
|
|
(lambda (&rest arguments)
|
|
(cl-incf calls)
|
|
(apply original arguments)))
|
|
((symbol-function 'ebox-surface--ensure-node-tree)
|
|
(lambda (&rest arguments)
|
|
(cl-incf ensured)
|
|
(apply original-ensure arguments)))
|
|
((symbol-function 'ebox--render-layout)
|
|
(lambda (node)
|
|
(push (plist-get node :node-id) rendered)
|
|
(funcall original-render node))))
|
|
(let* ((candidate (ebox-candidate-begin buffer))
|
|
(_replacement
|
|
(ebox-candidate-replace-host-ref
|
|
candidate 'target
|
|
(ebox-create :key 'card :class "card"
|
|
:host-ref 'target :content "After")))
|
|
(report (ebox-commit buffer candidate)))
|
|
(should (eq (plist-get report :projection-kind)
|
|
'owner-scoped))
|
|
;; The nested 1fr Grid now carries a strict local allocation
|
|
;; certificate; the proof no longer rescans every normal-flow
|
|
;; ancestor just to establish the same slot width.
|
|
(should (= 1 (plist-get report :ancestor-slot-count)))
|
|
(should (= 0 (plist-get report
|
|
:ancestor-slot-generation))))
|
|
(should (string-match-p
|
|
"After"
|
|
(substring-no-properties
|
|
(ebox-commit-test--buffer-string buffer))))
|
|
(should (= calls 1))
|
|
(should (= ensured 0))
|
|
(should (= (length rendered) 1))))
|
|
(when (buffer-live-p buffer)
|
|
(kill-buffer buffer))))))
|
|
|
|
(ert-deftest ebox-commit-keeps-root-owner-for-dynamic-cascade-source ()
|
|
"A TP-dependent cascade binding must preserve root-owner fallback."
|
|
(let* ((ebox-style-stylesheet (ecss-stylesheet-create))
|
|
(color (tp-signal-create "#111111"))
|
|
(calls 0)
|
|
(buffer nil))
|
|
(ebox-style-add-rule
|
|
".card" (list :color (tp-computed (lambda () (tp-signal-read color))))
|
|
:layer 'components)
|
|
(unwind-protect
|
|
(progn
|
|
(setq buffer
|
|
(ebox-render-to-buffer
|
|
(generate-new-buffer-name " *ebox-dynamic-style-proof*")
|
|
(ebox-column
|
|
(ebox-create :key 'card :class "card" :content "Before"
|
|
:width '(40))
|
|
(ebox-create :key 'stable :content "Stable"
|
|
:width '(40)))))
|
|
(let* ((state (ebox--buffer-render-state buffer))
|
|
(style-states (plist-get state :style-binding-states))
|
|
(binding-state
|
|
(cl-loop for value being the hash-values of style-states
|
|
when (and (plist-get value :binding)
|
|
(not (ebox-surface--static-style-state-p
|
|
value)))
|
|
return value)))
|
|
(should binding-state)
|
|
(should (> (tp-binding-dependency-count
|
|
(plist-get binding-state :binding))
|
|
(if (plist-get binding-state :parent-binding) 1 0)))
|
|
(should-not (ebox-surface--static-style-state-p binding-state)))
|
|
(let ((original
|
|
(symbol-function 'ebox-incremental--layout-owner-plan)))
|
|
(cl-letf (((symbol-function 'ebox-incremental--layout-owner-plan)
|
|
(lambda (&rest arguments)
|
|
(cl-incf calls)
|
|
(apply original arguments))))
|
|
(ebox-commit
|
|
buffer
|
|
(ebox-column
|
|
(ebox-create :key 'card :class "card" :content "After"
|
|
:width '(40))
|
|
(ebox-create :key 'stable :content "Stable"
|
|
:width '(40))))
|
|
(should (= calls 0)))))
|
|
(when (buffer-live-p buffer)
|
|
(kill-buffer buffer)))))
|
|
|
|
(ert-deftest ebox-owner-scoped-proof-rejects-unstable-allocation ()
|
|
"Auto intrinsic, line-count, and topology changes must keep root fallback."
|
|
(let ((ebox-style-stylesheet (ecss-stylesheet-create)))
|
|
(ebox-style-add-rule ".card" '(:color "#111111") :layer 'components)
|
|
(dolist
|
|
(case
|
|
(list
|
|
(list :name "auto"
|
|
:old (ebox-create :key 'card :class "card" :host-ref 'target
|
|
:content "Before")
|
|
:new (ebox-create :key 'card :class "card" :host-ref 'target
|
|
:content "After, intrinsically wider"))
|
|
(list :name "lines"
|
|
:old (ebox-create :key 'card :class "card" :host-ref 'target
|
|
:content "Before" :width '(40))
|
|
:new (ebox-create :key 'card :class "card" :host-ref 'target
|
|
:content "After\nsecond" :width '(40)))
|
|
(list :name "topology"
|
|
:old (ebox-create :key 'card :class "card" :host-ref 'target
|
|
:content "Before" :width '(40) :height 1)
|
|
:new (ebox-column
|
|
(ebox-create :key 'first :content "After" :width '(40))
|
|
(ebox-create :key 'second :content "Second"
|
|
:width '(40))))))
|
|
(let ((buffer nil))
|
|
(unwind-protect
|
|
(progn
|
|
(setq buffer
|
|
(ebox-render-to-buffer
|
|
(generate-new-buffer-name
|
|
(format " *ebox-owner-proof-%s*" (plist-get case :name)))
|
|
(ebox-column
|
|
(plist-get case :old)
|
|
(ebox-create :key 'stable :content "Stable"
|
|
:width '(40)))))
|
|
(let ((candidate (ebox-candidate-begin buffer)))
|
|
(ebox-candidate-replace-host-ref
|
|
candidate 'target (plist-get case :new))
|
|
(let ((report (ebox-commit buffer candidate)))
|
|
(should-not (eq (plist-get report :projection-kind)
|
|
'owner-scoped)))))
|
|
(when (buffer-live-p buffer)
|
|
(kill-buffer buffer)))))
|
|
(let ((buffer nil))
|
|
(unwind-protect
|
|
(progn
|
|
(setq buffer
|
|
(ebox-render-to-buffer
|
|
(generate-new-buffer-name " *ebox-owner-proof-auto-grid*")
|
|
(ebox-build
|
|
'(grid :width (100) :height 1
|
|
:grid-template-columns (auto (40))
|
|
(box :key card :class "card" :host-ref target
|
|
:content "Before" :height 1)
|
|
(box :key stable :content "Stable"
|
|
:width (40) :height 1)))))
|
|
(let ((candidate (ebox-candidate-begin buffer)))
|
|
(ebox-candidate-replace-host-ref
|
|
candidate 'target
|
|
(ebox-create :key 'card :class "card" :host-ref 'target
|
|
:content "After" :height 1))
|
|
(let ((report (ebox-commit buffer candidate)))
|
|
(should-not (eq (plist-get report :projection-kind)
|
|
'owner-scoped)))))
|
|
(when (buffer-live-p buffer)
|
|
(kill-buffer buffer))))))
|
|
|
|
(ert-deftest ebox-commit-owner-scoped-multi-owner-is-atomic ()
|
|
"Two disjoint fixed-slot owners render locally in one publication."
|
|
(let* ((ebox-style-stylesheet (ecss-stylesheet-create))
|
|
(buffer nil)
|
|
(ensured 0)
|
|
(ancestor-proofs 0)
|
|
(rendered nil)
|
|
(published 0))
|
|
(ebox-style-add-rule ".card" '(:color "#111111") :layer 'components)
|
|
(unwind-protect
|
|
(progn
|
|
(setq buffer
|
|
(ebox-render-to-buffer
|
|
(generate-new-buffer-name " *ebox-owner-multi*")
|
|
(ebox-build
|
|
'(grid :width (100)
|
|
:grid-template-columns (1fr 1fr)
|
|
(column :key card-shell :width stretch
|
|
:border "#687386" :padding (1 2)
|
|
(box :key first :class "card" :host-ref first
|
|
:content "First" )
|
|
(box :key second :class "card" :host-ref second
|
|
:content "Second"))
|
|
(box :key stable :content "Stable"
|
|
:width (40) :height 2)))))
|
|
(let* ((surface (with-current-buffer buffer
|
|
ebox-surface--buffer-surface))
|
|
(revision (tp-surface-revision surface))
|
|
(candidate (ebox-candidate-begin buffer))
|
|
(original-ensure
|
|
(symbol-function 'ebox-surface--ensure-node-tree))
|
|
(original-ancestor-proof
|
|
(symbol-function
|
|
'ebox-incremental--compute-ancestor-slot-signature))
|
|
(original-render (symbol-function 'ebox--render-layout)))
|
|
(ebox-candidate-replace-host-ref
|
|
candidate 'first
|
|
(ebox-create :key 'first :class "card" :host-ref 'first
|
|
:content "Alpha"))
|
|
(ebox-candidate-replace-host-ref
|
|
candidate 'second
|
|
(ebox-create :key 'second :class "card" :host-ref 'second
|
|
:content "Bravo!"))
|
|
(cl-letf (((symbol-function 'ebox-surface--ensure-node-tree)
|
|
(lambda (&rest arguments)
|
|
(cl-incf ensured)
|
|
(apply original-ensure arguments)))
|
|
((symbol-function 'ebox--render-layout)
|
|
(lambda (node)
|
|
(push (plist-get node :node-id) rendered)
|
|
(funcall original-render node)))
|
|
((symbol-function
|
|
'ebox-incremental--compute-ancestor-slot-signature)
|
|
(lambda (&rest arguments)
|
|
(cl-incf ancestor-proofs)
|
|
(apply original-ancestor-proof arguments))))
|
|
(let ((report
|
|
(ebox-commit
|
|
buffer candidate
|
|
(lambda (_report) (cl-incf published)))))
|
|
(should (eq (plist-get report :projection-kind)
|
|
'owner-scoped))
|
|
(should (= 2 (length (plist-get report
|
|
:rendered-owner-ids))))
|
|
(should (= 1 published))
|
|
(should (= 1 (- (tp-surface-revision surface) revision)))
|
|
(should (= 0 ensured))
|
|
(should (= 2 ancestor-proofs))
|
|
(should (= 2 (length rendered)))
|
|
(should (string-match-p
|
|
"Alpha" (ebox-commit-test--buffer-string buffer)))
|
|
(should (string-match-p
|
|
"Bravo!" (ebox-commit-test--buffer-string buffer)))
|
|
(let* ((text (ebox-commit-test--buffer-string buffer))
|
|
(position (string-match "Alpha" text)))
|
|
(should position)
|
|
(should (equal
|
|
(plist-get (get-text-property position 'face text)
|
|
:foreground)
|
|
"#111111")))
|
|
(should (< (plist-get report :reconciled-objects)
|
|
(plist-get (tp-surface-inspect surface)
|
|
:object-count)))))
|
|
(let* ((fallback-revision (tp-surface-revision surface))
|
|
(fallback (ebox-candidate-begin buffer)))
|
|
(ebox-candidate-replace-host-ref
|
|
fallback 'first
|
|
(ebox-create :key 'first :class "card" :host-ref 'first
|
|
:content "Gamma"))
|
|
(ebox-candidate-replace-host-ref
|
|
fallback 'second
|
|
(ebox-create :key 'second :class "card" :host-ref 'second
|
|
:content "Much much much much much much longer beta content"))
|
|
(let ((report (ebox-commit buffer fallback)))
|
|
(should-not (eq (plist-get report :projection-kind)
|
|
'owner-scoped))
|
|
(should (or (plist-get report :tp-scope-fallback)
|
|
(plist-get report :tp-full-root)))
|
|
(should (= 1 (- (tp-surface-revision surface)
|
|
fallback-revision)))
|
|
(let ((text (ebox-commit-test--buffer-string buffer)))
|
|
(should (string-match-p
|
|
"Much much much much much much longer beta"
|
|
text))
|
|
(should (string-match-p "content" text)))))))
|
|
(when (buffer-live-p buffer)
|
|
(kill-buffer buffer)))))
|
|
|
|
(ert-deftest ebox-commit-formatting-context-reflow-owns-variable-line-siblings ()
|
|
"Two variable-line owners should publish through their nearest stack context.
|
|
The context owns the complete local block; the root and untouched header/footer
|
|
remain retained identities."
|
|
(let* ((old-root
|
|
(ebox-column
|
|
(ebox-create :key 'header :content "Header" :width '(160))
|
|
(ebox-create
|
|
:key 'shell :width '(160)
|
|
:ebox-content-node
|
|
(ebox-column
|
|
(ebox-create :key 'message :host-ref 'message
|
|
:content "Callback action pending")
|
|
(ebox-flex :width '(120) :height 1
|
|
(ebox-create :key 'toggle :host-ref 'toggle
|
|
:content "Behavior: off"))))
|
|
(ebox-create :key 'footer :content "Footer" :width '(160))))
|
|
(new-message
|
|
(ebox-create :key 'message :host-ref 'message
|
|
:content "Behavior toggle: on / callback active / a longer status line"))
|
|
(new-toggle
|
|
(ebox-create :key 'toggle :host-ref 'toggle
|
|
:content "Behavior: on"))
|
|
(new-root
|
|
(ebox-column
|
|
(ebox-create :key 'header :content "Header" :width '(160))
|
|
(ebox-create
|
|
:key 'shell :width '(160)
|
|
:ebox-content-node
|
|
(ebox-column
|
|
new-message
|
|
(ebox-flex :width '(120) :height 1 new-toggle)))
|
|
(ebox-create :key 'footer :content "Footer" :width '(160))))
|
|
(buffer nil)
|
|
(fresh nil))
|
|
(unwind-protect
|
|
(progn
|
|
(setq buffer
|
|
(ebox-render-to-buffer
|
|
(generate-new-buffer-name
|
|
" *ebox-formatting-context-reflow*")
|
|
old-root))
|
|
(let ((candidate (ebox-candidate-begin buffer)))
|
|
(ebox-candidate-replace-host-ref candidate 'message new-message)
|
|
(ebox-candidate-replace-host-ref candidate 'toggle new-toggle)
|
|
(let ((root-id
|
|
(plist-get (plist-get (ebox--buffer-render-state buffer)
|
|
:root-node)
|
|
:node-id))
|
|
report)
|
|
(setq report (ebox-commit buffer candidate))
|
|
(should (eq (plist-get report :projection-kind)
|
|
'formatting-context-reflow))
|
|
(should (= 1 (length (plist-get report :owner-ids))))
|
|
(should-not (member root-id (plist-get report :owner-ids)))
|
|
(should-not (plist-get report :tp-full-root))
|
|
(should-not (plist-get report :tp-scope-fallback))
|
|
(should (= 1 (plist-get report :tp-scope-count)))
|
|
(should (= 1 (plist-get report :tp-scope-range-count)))
|
|
(should (= 1 (plist-get report :tp-text-operations)))))
|
|
(setq fresh
|
|
(ebox-render-to-buffer
|
|
(generate-new-buffer-name
|
|
" *ebox-formatting-context-fresh*")
|
|
new-root))
|
|
(let* ((committed (ebox-commit-test--buffer-string buffer))
|
|
(expected (ebox-commit-test--buffer-string fresh))
|
|
(keys '(ebox-content ebox-content-idx ebox-content-owner
|
|
ebox-content-owners display))
|
|
(semantic-owner
|
|
(lambda (state value)
|
|
(if (numberp value)
|
|
(let* ((region-node-table
|
|
(plist-get state :region-node-table))
|
|
(node-table (plist-get state :node-table))
|
|
(node-id (and region-node-table
|
|
(gethash value region-node-table)))
|
|
(node (and node-id
|
|
(gethash node-id node-table))))
|
|
(or (and node (ebox-tree-node-key node)) value))
|
|
value)))
|
|
(semantic-properties
|
|
(lambda (state text position)
|
|
(mapcar
|
|
(lambda (key)
|
|
(cons key
|
|
(let ((value (get-text-property
|
|
position key text)))
|
|
(if (memq key '(ebox-content-owner
|
|
ebox-content-owners
|
|
ebox-content))
|
|
(if (listp value)
|
|
(mapcar (lambda (owner)
|
|
(funcall semantic-owner
|
|
state owner))
|
|
value)
|
|
(funcall semantic-owner state value))
|
|
value))))
|
|
keys))))
|
|
;; Region ids are buffer-local allocation identities. Compare
|
|
;; stable node semantics and layout properties, not those ids.
|
|
(should (equal (substring-no-properties committed)
|
|
(substring-no-properties expected)))
|
|
(should (= (length committed) (length expected)))
|
|
(dotimes (position (length committed))
|
|
(should
|
|
(equal (funcall semantic-properties
|
|
(ebox--buffer-render-state buffer)
|
|
committed position)
|
|
(funcall semantic-properties
|
|
(ebox--buffer-render-state fresh)
|
|
expected position))))))
|
|
(when (buffer-live-p buffer)
|
|
(kill-buffer buffer))
|
|
(when (buffer-live-p fresh)
|
|
(kill-buffer fresh)))))
|
|
|
|
(ert-deftest ebox-commit-formatting-context-reflow-rolls-back-and-retries ()
|
|
"Formatting-context reflow keeps one rollback boundary and can retry."
|
|
(cl-labels
|
|
((root (message toggle)
|
|
(ebox-column
|
|
(ebox-create :key 'header :content "Header" :width '(160))
|
|
(ebox-create
|
|
:key 'shell :width '(160)
|
|
:ebox-content-node
|
|
(ebox-column
|
|
(ebox-create :key 'message :host-ref 'message :content message)
|
|
(ebox-flex :width '(120) :height 1
|
|
(ebox-create :key 'toggle :host-ref 'toggle
|
|
:content toggle))))
|
|
(ebox-create :key 'footer :content "Footer" :width '(160)))))
|
|
(dolist (failure-kind '(client-state final-accept))
|
|
(let ((buffer (generate-new-buffer
|
|
(format " *ebox-formatting-context-%S*" failure-kind))))
|
|
(unwind-protect
|
|
(progn
|
|
(ebox-render-to-buffer
|
|
buffer (root "Callback action pending" "Behavior: off"))
|
|
(let* ((before (ebox-commit-test--buffer-string buffer))
|
|
(state (tp-surface-client-state
|
|
(with-current-buffer
|
|
buffer ebox-surface--buffer-surface)))
|
|
(revision (tp-surface-revision
|
|
(with-current-buffer
|
|
buffer ebox-surface--buffer-surface)))
|
|
(candidate (ebox-candidate-begin buffer)))
|
|
(ebox-candidate-replace-host-ref
|
|
candidate 'message
|
|
(ebox-create
|
|
:key 'message :host-ref 'message
|
|
:content "Behavior toggle: on / callback active / a longer status line"))
|
|
(ebox-candidate-replace-host-ref
|
|
candidate 'toggle
|
|
(ebox-create :key 'toggle :host-ref 'toggle
|
|
:content "Behavior: on"))
|
|
(if (eq failure-kind 'client-state)
|
|
(let ((tp--surface-publication-step-function
|
|
(lambda (step _surface)
|
|
(when (eq step 'client-state)
|
|
(error "reject formatting reflow publication")))))
|
|
(should-error (ebox-commit buffer candidate)))
|
|
(cl-letf (((symbol-function 'accept-change-group)
|
|
(lambda (_group)
|
|
(error "reject formatting reflow accept"))))
|
|
(should-error (ebox-commit buffer candidate))))
|
|
(let ((surface (with-current-buffer
|
|
buffer ebox-surface--buffer-surface)))
|
|
(should (eq (tp-surface-client-state surface) state))
|
|
(should (= (tp-surface-revision surface) revision))
|
|
(should (equal-including-properties
|
|
(ebox-commit-test--buffer-string buffer) before)))
|
|
(let ((retry (ebox-candidate-begin buffer)))
|
|
(ebox-candidate-replace-host-ref
|
|
retry 'message
|
|
(ebox-create
|
|
:key 'message :host-ref 'message
|
|
:content "Behavior toggle: on / callback active / a longer status line"))
|
|
(ebox-candidate-replace-host-ref
|
|
retry 'toggle
|
|
(ebox-create :key 'toggle :host-ref 'toggle
|
|
:content "Behavior: on"))
|
|
(let ((report (ebox-commit buffer retry)))
|
|
(should (eq (plist-get report :projection-kind)
|
|
'formatting-context-reflow))
|
|
(should-not (plist-get report :tp-scope-fallback))
|
|
(should-not (plist-get report :tp-full-root))))))
|
|
(when (buffer-live-p buffer)
|
|
(kill-buffer buffer)))))))
|
|
|
|
(ert-deftest ebox-commit-multi-owner-variable-content-keeps-fixed-slots ()
|
|
"Two fixed Grid slots accept unequal one-line content in one publication."
|
|
(let* ((new-left
|
|
(ebox-create :key 'left :host-ref 'left
|
|
:content "L"))
|
|
(new-right
|
|
(ebox-create :key 'right :host-ref 'right :content "R"))
|
|
(old-root
|
|
(ebox-build
|
|
'(grid :key grid :width (80)
|
|
:grid-template-columns ((36) (36))
|
|
:column-gap (0 (8))
|
|
(box :key left :host-ref left :content "left-old")
|
|
(box :key right :host-ref right :content "right-old"))))
|
|
(buffer nil)
|
|
(fresh nil)
|
|
report)
|
|
(unwind-protect
|
|
(progn
|
|
(setq buffer
|
|
(ebox-render-to-buffer
|
|
(generate-new-buffer-name
|
|
" *ebox-multi-owner-variable-slots*")
|
|
old-root))
|
|
(let ((candidate (ebox-candidate-begin buffer)))
|
|
(ebox-candidate-replace-host-ref candidate 'left new-left)
|
|
(ebox-candidate-replace-host-ref candidate 'right new-right)
|
|
(setq report (ebox-commit buffer candidate)))
|
|
(should (memq (plist-get report :projection-kind)
|
|
'(span-patch owner-scoped)))
|
|
(should-not (plist-get report :tp-full-root))
|
|
(should-not (plist-get report :tp-scope-fallback))
|
|
(should (= 1 (plist-get report :tp-text-operations)))
|
|
(should (string-match-p "L"
|
|
(ebox-commit-test--buffer-string buffer)))
|
|
(setq fresh
|
|
(ebox-render-to-buffer
|
|
(generate-new-buffer-name
|
|
" *ebox-multi-owner-variable-slots-fresh*")
|
|
(ebox-build
|
|
'(grid :key grid :width (80)
|
|
:grid-template-columns ((36) (36))
|
|
:column-gap (0 (8))
|
|
(box :key left :host-ref left :content "L")
|
|
(box :key right :host-ref right :content "R")))))
|
|
(should (equal
|
|
(substring-no-properties
|
|
(ebox-commit-test--buffer-string buffer))
|
|
(substring-no-properties
|
|
(ebox-commit-test--buffer-string fresh))))
|
|
(when (buffer-live-p buffer)
|
|
(kill-buffer buffer))
|
|
(when (buffer-live-p fresh)
|
|
(kill-buffer fresh))))))
|
|
|
|
(ert-deftest ebox-commit-multi-owner-variable-content-rolls-back-and-retries ()
|
|
"Variable multi-owner spans restore old state at both TP failure points."
|
|
(cl-labels
|
|
((root (left right)
|
|
(ebox-grid :key 'grid :width '(80)
|
|
:grid-template-columns '((36) (36))
|
|
:column-gap '(0 (8))
|
|
(ebox-create :key 'left :host-ref 'left :content left)
|
|
(ebox-create :key 'right :host-ref 'right :content right)))
|
|
(candidate (buffer)
|
|
(let ((candidate (ebox-candidate-begin buffer)))
|
|
(ebox-candidate-replace-host-ref
|
|
candidate 'left
|
|
(ebox-create :key 'left :host-ref 'left :content "L"))
|
|
(ebox-candidate-replace-host-ref
|
|
candidate 'right
|
|
(ebox-create :key 'right :host-ref 'right :content "R"))
|
|
candidate)))
|
|
(dolist (failure-kind '(client-state final-accept))
|
|
(let ((buffer (generate-new-buffer
|
|
(format " *ebox-variable-span-%S*" failure-kind))))
|
|
(unwind-protect
|
|
(progn
|
|
(ebox-render-to-buffer
|
|
buffer (root "left-old" "right-old"))
|
|
(let* ((surface (with-current-buffer
|
|
buffer ebox-surface--buffer-surface))
|
|
(state (tp-surface-client-state surface))
|
|
(revision (tp-surface-revision surface))
|
|
(before (ebox-commit-test--buffer-string buffer))
|
|
(candidate (candidate buffer)))
|
|
(if (eq failure-kind 'client-state)
|
|
(let ((tp--surface-publication-step-function
|
|
(lambda (step _surface)
|
|
(when (eq step 'client-state)
|
|
(error "reject variable span publication")))))
|
|
(should-error (ebox-commit buffer candidate)))
|
|
(cl-letf (((symbol-function 'accept-change-group)
|
|
(lambda (_group)
|
|
(error "reject variable span accept"))))
|
|
(should-error (ebox-commit buffer candidate))))
|
|
(should (eq (tp-surface-client-state surface) state))
|
|
(should (= (tp-surface-revision surface) revision))
|
|
(should (equal-including-properties
|
|
(ebox-commit-test--buffer-string buffer) before))
|
|
(let ((report (ebox-commit buffer (candidate buffer))))
|
|
(should (memq (plist-get report :projection-kind)
|
|
'(span-patch owner-scoped)))
|
|
(should-not (plist-get report :tp-full-root))
|
|
(should-not (plist-get report :tp-scope-fallback)))))
|
|
(when (buffer-live-p buffer)
|
|
(kill-buffer buffer)))))))
|
|
|
|
(ert-deftest ebox-commit-updates-raw-runtime-type-counts ()
|
|
"A structural candidate should publish exact raw runtime type counts."
|
|
(let* ((buffer
|
|
(ebox-render-to-buffer
|
|
(generate-new-buffer-name " *ebox-commit-types*")
|
|
(ebox-build
|
|
'(flex :key root :width (80)
|
|
(item (box :key child :content "A" :width (40)))))))
|
|
(before (gethash buffer ebox--buffer-render-state-table)))
|
|
(unwind-protect
|
|
(progn
|
|
(should (= (gethash
|
|
'item (plist-get before :runtime-type-count-table))
|
|
1))
|
|
(ebox-commit
|
|
buffer (ebox-create :key 'root :content "B" :width '(80)))
|
|
(let ((after (gethash buffer ebox--buffer-render-state-table)))
|
|
(should-not
|
|
(gethash 'item (plist-get after :runtime-type-count-table)))
|
|
(should (= (gethash
|
|
'box (plist-get after :runtime-type-count-table))
|
|
1))))
|
|
(when (buffer-live-p buffer)
|
|
(kill-buffer buffer)))))
|
|
|
|
(ert-deftest ebox-commit-rolls-back-on-invalid-root ()
|
|
"A failed candidate must leave the previously published buffer intact."
|
|
(let ((buffer (ebox-render-to-buffer
|
|
(generate-new-buffer-name " *ebox-commit-rollback*")
|
|
(ebox-create :key 'root :content "Stable" :width '(80)))))
|
|
(unwind-protect
|
|
(progn
|
|
(should-error
|
|
(ebox-commit buffer (ebox-create :key 'root :content nil
|
|
:width 'invalid)))
|
|
(should (string-prefix-p
|
|
"Stable"
|
|
(string-trim-right
|
|
(substring-no-properties
|
|
(ebox-commit-test--buffer-string buffer))))))
|
|
(when (buffer-live-p buffer)
|
|
(kill-buffer buffer)))))
|
|
|
|
(ert-deftest ebox-candidate-root-replacement-reuses-candidate-validity-contract ()
|
|
"Root replacement rejects invalid, other-buffer, stale, and sealed use."
|
|
(let ((first (generate-new-buffer " *ebox-root-valid-first*"))
|
|
(second (generate-new-buffer " *ebox-root-valid-second*")))
|
|
(unwind-protect
|
|
(progn
|
|
(ebox-render-to-buffer first (ebox-create :key 'root :content "one"))
|
|
(ebox-render-to-buffer second (ebox-create :key 'root :content "two"))
|
|
(let ((candidate (ebox-candidate-begin first)))
|
|
(should-error (ebox-candidate-replace-root candidate "invalid"))
|
|
(ebox-candidate-replace-root
|
|
candidate (ebox-create :key 'root :content "candidate"))
|
|
(should-error (ebox-commit second candidate))
|
|
(should-error
|
|
(ebox-candidate-replace-root
|
|
candidate (ebox-create :key 'root :content "sealed"))))
|
|
(let ((candidate (ebox-candidate-begin first)))
|
|
(ebox-candidate-replace-root
|
|
candidate (ebox-create :key 'root :content "stale"))
|
|
(ebox-commit first (ebox-create :key 'root :content "new-base"))
|
|
(should-error (ebox-commit first candidate))))
|
|
(dolist (buffer (list first second))
|
|
(when (buffer-live-p buffer) (kill-buffer buffer))))))
|
|
|
|
(ert-deftest ebox-commit-two-and-three-argument-compatibility ()
|
|
"Two-argument commits complete and legacy callbacks receive one same report."
|
|
(let ((buffer (generate-new-buffer " *ebox-framework-compat*")))
|
|
(unwind-protect
|
|
(progn
|
|
(ebox-render-to-buffer buffer (ebox-create :key 'root :content "0"))
|
|
(should (eq (plist-get
|
|
(ebox-commit buffer (ebox-create :key 'root :content "1"))
|
|
:framework-participant-state)
|
|
'completed))
|
|
(let ((calls 0) seen)
|
|
(let ((report
|
|
(ebox-commit
|
|
buffer (ebox-create :key 'root :content "2")
|
|
(lambda (value) (cl-incf calls) (setq seen value)))))
|
|
(should (= calls 1))
|
|
(should (eq seen report))
|
|
(should (eq (plist-get seen :framework-participant-state)
|
|
'completed)))))
|
|
(when (buffer-live-p buffer) (kill-buffer buffer)))))
|
|
|
|
(ert-deftest ebox-commit-final-accept-failure-rolls-framework-back ()
|
|
"A TP final-accept failure rolls the paired framework pointer back once."
|
|
(dolist (mode '(full scoped))
|
|
(let ((buffer (generate-new-buffer " *ebox-framework-accept-fail*")))
|
|
(unwind-protect
|
|
(progn
|
|
(ebox-render-to-buffer
|
|
buffer
|
|
(ebox-column
|
|
(ebox-create :key 'a :host-ref 'a :content "old-a")
|
|
(ebox-create :key 'b :host-ref 'b :content "old-b")))
|
|
(let* ((before (ebox-commit-test--buffer-string buffer))
|
|
(candidate (ebox-candidate-begin buffer))
|
|
trace captured failure)
|
|
(ebox-candidate-replace-host-ref
|
|
candidate 'a (ebox-create :key 'a :host-ref 'a :content "new-a"))
|
|
(cl-letf (((symbol-function 'accept-change-group)
|
|
(lambda (_group) (error "accept failed"))))
|
|
(setq failure
|
|
(condition-case condition
|
|
(ebox-commit
|
|
buffer
|
|
(if (eq mode 'scoped)
|
|
candidate
|
|
(ebox-create :key 'root :content "new-root"))
|
|
(lambda (report)
|
|
(setq captured report)
|
|
(push 'publish trace))
|
|
(lambda (_report)
|
|
(push 'rollback trace)
|
|
(signal 'quit nil)))
|
|
(error condition))))
|
|
(should (equal (cadr failure) "accept failed"))
|
|
(should (equal trace '(rollback publish)))
|
|
(should (eq (plist-get captured :framework-participant-state)
|
|
'rolled-back))
|
|
(should (= (length
|
|
(plist-get captured
|
|
:framework-participant-diagnostics))
|
|
1))
|
|
(should (equal-including-properties
|
|
(ebox-commit-test--buffer-string buffer) before))
|
|
(should (eq (plist-get
|
|
(ebox-commit
|
|
buffer (ebox-create :key 'root :content "next"))
|
|
:framework-participant-state)
|
|
'completed))))
|
|
(when (buffer-live-p buffer) (kill-buffer buffer))))))
|
|
|
|
(ert-deftest ebox-commit-records-scroll-diagnostics-before-completion ()
|
|
"Contained scroll failures are separately reported and do not block retry."
|
|
(let ((buffer (generate-new-buffer " *ebox-scroll-diagnostics*"))
|
|
(diagnostics
|
|
'((:region-id one :phase scroll-finalization :action cancel
|
|
:condition (error "x"))
|
|
(:region-id one :phase scroll-finalization :action stop
|
|
:condition (quit)))))
|
|
(unwind-protect
|
|
(progn
|
|
(ebox-render-to-buffer buffer (ebox-create :key 'root :content "0"))
|
|
(cl-letf
|
|
(((symbol-function
|
|
'ebox-incremental--finalize-declarative-scroll-publication)
|
|
(lambda (&rest _arguments) diagnostics)))
|
|
(let ((report
|
|
(ebox-commit
|
|
buffer (ebox-create :key 'root :content "1"))))
|
|
(should (eq (plist-get report :framework-participant-state)
|
|
'completed))
|
|
(should (equal
|
|
(plist-get report :scroll-finalization-diagnostics)
|
|
diagnostics))))
|
|
(should (eq (plist-get
|
|
(ebox-commit buffer
|
|
(ebox-create :key 'root :content "2"))
|
|
:framework-participant-state)
|
|
'completed)))
|
|
(when (buffer-live-p buffer) (kill-buffer buffer)))))
|
|
|
|
(ert-deftest ebox-commit-restores-ebox-after-participant-owner-failure ()
|
|
"A rollback-owner failure cannot skip restoration of Ebox runtime state."
|
|
(let ((buffer (generate-new-buffer " *ebox-participant-owner-failure*")))
|
|
(unwind-protect
|
|
(progn
|
|
(ebox-render-to-buffer buffer (ebox-create :key 'root :content "old"))
|
|
(let* ((surface (with-current-buffer
|
|
buffer ebox-surface--buffer-surface))
|
|
(old-state (tp-surface-client-state surface))
|
|
(old-buffer (ebox-commit-test--buffer-string buffer))
|
|
(original-report
|
|
(symbol-function 'ebox-surface--participant-report))
|
|
rollback-called failure)
|
|
(cl-letf
|
|
(((symbol-function 'accept-change-group)
|
|
(lambda (_group) (error "primary accept failure")))
|
|
((symbol-function 'ebox-surface--participant-report)
|
|
(lambda (participant report state)
|
|
(prog1 (funcall original-report participant report state)
|
|
(when (and rollback-called (eq state 'rolled-back))
|
|
(error "rollback owner failure"))))))
|
|
(setq failure
|
|
(condition-case condition
|
|
(ebox-commit
|
|
buffer (ebox-create :key 'root :content "new")
|
|
#'ignore
|
|
(lambda (_report) (setq rollback-called t)))
|
|
(error condition))))
|
|
(should (equal (cadr failure) "primary accept failure"))
|
|
(should rollback-called)
|
|
(should
|
|
(tp--transaction-condition-trailer failure :rollback-failures))
|
|
(should (eq (tp-surface-client-state surface) old-state))
|
|
(should (eq (ebox--buffer-render-state buffer) old-state))
|
|
(should (equal-including-properties
|
|
(ebox-commit-test--buffer-string buffer) old-buffer))))
|
|
(when (buffer-live-p buffer) (kill-buffer buffer)))))
|
|
|
|
(defun ebox-commit-test--mixed-owner-root
|
|
(left right paint-a paint-b paint-c)
|
|
"Return the raw sibling fixture for mixed geometry/paint publication."
|
|
(ebox-column
|
|
(ebox-create
|
|
:key 'geometry-context :width '(80)
|
|
:ebox-content-node
|
|
(ebox-column
|
|
(ebox-create :key 'left :host-ref 'left
|
|
:content left :font 'bold)
|
|
(ebox-create :key 'right :host-ref 'right
|
|
:content right)))
|
|
(ebox-create :key 'paint-a :host-ref 'paint-a
|
|
:content "paint-a" :color paint-a)
|
|
(ebox-create :key 'paint-b :host-ref 'paint-b
|
|
:content "paint-b" :bgcolor paint-b)
|
|
(ebox-create :key 'paint-c :host-ref 'paint-c
|
|
:content "paint-c" :color paint-c)
|
|
(ebox-create :key 'untouched :host-ref 'untouched
|
|
:content "untouched")))
|
|
|
|
(ert-deftest ebox-candidate-host-paint-patch-preserves-subtree-identity ()
|
|
"Patch one Host's paint without copying or reconciling its descendants."
|
|
(let* ((buffer
|
|
(ebox-render-to-buffer
|
|
(generate-new-buffer-name " *ebox-host-paint-patch* ")
|
|
(ebox-create
|
|
:key 'target :host-ref 'target :bgcolor "#111111"
|
|
:ebox-content-node
|
|
(ebox-column
|
|
(ebox-create :key 'child :host-ref 'child :content "child")))))
|
|
(child-id (plist-get (ebox--host-ref-node buffer 'child) :node-id)))
|
|
(unwind-protect
|
|
(progn
|
|
(let ((candidate (ebox-candidate-begin buffer)))
|
|
(ebox-candidate-patch-host-paint
|
|
candidate 'target
|
|
(ebox-create
|
|
:key 'target :host-ref 'target :bgcolor "#111111"
|
|
:ebox-content-node
|
|
(ebox-column
|
|
(ebox-create :key 'child :host-ref 'child :content "child")))
|
|
(ebox-create
|
|
:key 'target :host-ref 'target :bgcolor "#EEEEEE"
|
|
:ebox-content-node
|
|
(ebox-column
|
|
(ebox-create :key 'child :host-ref 'child :content "child"))))
|
|
(let ((report (ebox-commit buffer candidate)))
|
|
(should (eq (plist-get report :projection-kind) 'paint))
|
|
(should (= child-id
|
|
(plist-get (ebox--host-ref-node buffer 'child)
|
|
:node-id)))
|
|
(should (equal "#EEEEEE"
|
|
(plist-get (ebox--host-ref-node buffer 'target)
|
|
:bgcolor)))))
|
|
(let ((candidate (ebox-candidate-begin buffer)))
|
|
(should-not
|
|
(ebox-candidate-patch-host-paint
|
|
candidate 'target
|
|
(ebox-create
|
|
:key 'target :host-ref 'target :bgcolor "#EEEEEE"
|
|
:content "child")
|
|
(ebox-create
|
|
:key 'target :host-ref 'target :bgcolor "#EEEEEE"
|
|
:width '(40) :content "child"))))
|
|
(let ((candidate (ebox-candidate-begin buffer)))
|
|
(should
|
|
(ebox-candidate-patch-host-paint
|
|
candidate 'target
|
|
(ebox-create
|
|
:key 'target :host-ref 'target :bgcolor "#EEEEEE"
|
|
:ebox-content-node
|
|
(ebox-column
|
|
(ebox-create :key 'child :host-ref 'child :content "child")))
|
|
(ebox-create
|
|
:key 'target :host-ref 'target :bgcolor "#EEEEEE"
|
|
:color "#FFFFFF"
|
|
:ebox-content-node
|
|
(ebox-column
|
|
(ebox-create :key 'child :host-ref 'child :content "child")))))
|
|
(ebox-commit buffer candidate)
|
|
(should
|
|
(equal "#FFFFFF"
|
|
(plist-get (ebox--host-ref-node buffer 'target) :color)))
|
|
(should
|
|
(equal "#FFFFFF"
|
|
(plist-get (ebox--host-ref-node buffer 'child) :color)))))
|
|
(when (buffer-live-p buffer) (kill-buffer buffer)))))
|
|
|
|
(defun ebox-commit-test--fixed-basis-selection-root (row-1 row-2)
|
|
"Return a stretched fixed-basis panel containing two selectable rows."
|
|
(let ((panel
|
|
(ebox-create
|
|
:key 'selection-panel :width 'stretch :min-width 0 :min-height 24
|
|
:flex-grow 2 :flex-shrink 1 :flex-basis '(340)
|
|
:ebox-content-node
|
|
(ebox-column
|
|
(ebox-create :key 'row-1 :host-ref 'row-1 :content row-1)
|
|
(ebox-create :key 'row-2 :host-ref 'row-2 :content row-2)))))
|
|
(ebox-flex
|
|
:key 'fixed-basis-selection-root
|
|
:width '(900) :height 24
|
|
:flex-flow '(row nowrap) :align-items 'stretch
|
|
(ebox-create :key 'peer :content "peer" :width 'stretch
|
|
:min-width 0 :min-height 24
|
|
:flex-grow 4 :flex-shrink 1 :flex-basis '(620))
|
|
panel)))
|
|
|
|
(defun ebox-commit-test--fixed-basis-selection-candidate
|
|
(buffer row-1 row-2)
|
|
"Return BUFFER candidate replacing both fixed-basis selection rows."
|
|
(let ((candidate (ebox-candidate-begin buffer)))
|
|
(ebox-candidate-replace-host-ref
|
|
candidate 'row-1
|
|
(ebox-create :key 'row-1 :host-ref 'row-1 :content row-1))
|
|
(ebox-candidate-replace-host-ref
|
|
candidate 'row-2
|
|
(ebox-create :key 'row-2 :host-ref 'row-2 :content row-2))
|
|
candidate))
|
|
|
|
(ert-deftest ebox-commit-fixed-basis-selection-round-trip-stays-local ()
|
|
"Continuous row-1 -> row-2 -> row-1 publication keeps local TP scope."
|
|
(let ((slot-proof-count 0)
|
|
(original-slot-proof
|
|
(symbol-function 'ebox--flex-item-slot-footprint-safe-p))
|
|
(buffer
|
|
(ebox-render-to-buffer
|
|
(generate-new-buffer-name " *ebox-fixed-basis-round-trip* ")
|
|
(ebox-commit-test--fixed-basis-selection-root
|
|
"[x] row 1" "[ ] row 2"))))
|
|
(unwind-protect
|
|
(let* ((root-id (ebox--buffer-root-node-id buffer))
|
|
(panel-id
|
|
(plist-get (ebox--host-ref-node buffer 'row-1) :node-id))
|
|
reports)
|
|
(setq panel-id
|
|
(ebox-incremental--nearest-fixed-basis-flex-item-owner-id
|
|
buffer panel-id))
|
|
(cl-letf (((symbol-function 'ebox--flex-item-slot-footprint-safe-p)
|
|
(lambda (&rest arguments)
|
|
(cl-incf slot-proof-count)
|
|
(apply original-slot-proof arguments))))
|
|
(dolist (contents '(("[ ] row 1" "[x] row 2")
|
|
("[x] row 1" "[ ] row 2")))
|
|
(let* ((report
|
|
(ebox-commit
|
|
buffer
|
|
(ebox-commit-test--fixed-basis-selection-candidate
|
|
buffer (car contents) (cadr contents))))
|
|
(surface (with-current-buffer
|
|
buffer ebox-surface--buffer-surface))
|
|
(tp-report (tp-surface-report surface))
|
|
(object-count
|
|
(plist-get (tp-surface-inspect surface) :object-count))
|
|
(snapshots
|
|
(plist-get (ebox--buffer-render-state buffer)
|
|
:layout-snapshots))
|
|
(panel-snapshot (and snapshots
|
|
(gethash panel-id snapshots))))
|
|
(push report reports)
|
|
(should (eq (plist-get report :projection-kind)
|
|
'owner-scoped))
|
|
(should-not (member root-id (plist-get report :owner-ids)))
|
|
(should-not (plist-get report :tp-full-root))
|
|
(should-not (plist-get report :tp-scope-fallback))
|
|
(should (< (plist-get tp-report :reconciled-objects)
|
|
object-count))
|
|
(should (<= (plist-get tp-report :reconciled-objects) 4))
|
|
;; Successful publication leaves the fixed-basis owner ready
|
|
;; to recapture geometry from the committed TP mounts.
|
|
(should panel-snapshot)
|
|
(should-not (plist-member panel-snapshot :buffer-spans))
|
|
(with-current-buffer buffer
|
|
(should
|
|
(equal
|
|
(buffer-substring-no-properties (point-min) (point-max))
|
|
(substring-no-properties
|
|
(ebox-render (ebox--buffer-root-node buffer)))))))))
|
|
(should (= (length reports) 2))
|
|
(should (= slot-proof-count 2)))
|
|
(when (buffer-live-p buffer) (kill-buffer buffer)))))
|
|
|
|
(defun ebox-commit-test--mixed-owner-candidate
|
|
(buffer left right paint-a paint-b paint-c)
|
|
"Return BUFFER candidate replacing all mixed fixture owners."
|
|
(let ((candidate (ebox-candidate-begin buffer)))
|
|
(ebox-candidate-replace-host-ref
|
|
candidate 'left
|
|
(ebox-create :key 'left :host-ref 'left
|
|
:content left :font 'bold))
|
|
(ebox-candidate-replace-host-ref
|
|
candidate 'right
|
|
(ebox-create :key 'right :host-ref 'right
|
|
:content right))
|
|
(ebox-candidate-replace-host-ref
|
|
candidate 'paint-a
|
|
(ebox-create :key 'paint-a :host-ref 'paint-a
|
|
:content "paint-a" :color paint-a))
|
|
(ebox-candidate-replace-host-ref
|
|
candidate 'paint-b
|
|
(ebox-create :key 'paint-b :host-ref 'paint-b
|
|
:content "paint-b" :bgcolor paint-b))
|
|
(ebox-candidate-replace-host-ref
|
|
candidate 'paint-c
|
|
(ebox-create :key 'paint-c :host-ref 'paint-c
|
|
:content "paint-c" :color paint-c))
|
|
candidate))
|
|
|
|
(ert-deftest ebox-commit-mixed-owner-reflow-is-atomic-and-round-trips ()
|
|
"Geometry and color owners publish once without duplicating face layers."
|
|
(let ((buffer
|
|
(ebox-render-to-buffer
|
|
(generate-new-buffer-name " *ebox-mixed-owner* ")
|
|
(ebox-commit-test--mixed-owner-root
|
|
"left-old" "right-old" "#111111" "#222222" "#333333"))))
|
|
(unwind-protect
|
|
(progn
|
|
(let ((candidate (ebox-candidate-begin buffer)))
|
|
(ebox-candidate-replace-host-ref
|
|
candidate 'left
|
|
(ebox-create :key 'left :host-ref 'left
|
|
:content "left-new" :font 'bold))
|
|
(ebox-candidate-replace-host-ref
|
|
candidate 'right
|
|
(ebox-create :key 'right :host-ref 'right
|
|
:content "right-new"))
|
|
(ebox-candidate-replace-host-ref
|
|
candidate 'paint-a
|
|
(ebox-create :key 'paint-a :host-ref 'paint-a
|
|
:content "paint-a" :color "#AAAAAA"))
|
|
(ebox-candidate-replace-host-ref
|
|
candidate 'paint-b
|
|
(ebox-create :key 'paint-b :host-ref 'paint-b
|
|
:content "paint-b" :bgcolor "#BBBBBB"))
|
|
(ebox-candidate-replace-host-ref
|
|
candidate 'paint-c
|
|
(ebox-create :key 'paint-c :host-ref 'paint-c
|
|
:content "paint-c" :color "#CCCCCC"))
|
|
(let ((report (ebox-commit buffer candidate)))
|
|
(should (eq (plist-get report :projection-kind)
|
|
'mixed-owner-reflow))
|
|
(should-not (plist-get report :tp-full-root))
|
|
(should-not (plist-get report :tp-scope-fallback))
|
|
(should-not
|
|
(member (ebox--buffer-root-node-id buffer)
|
|
(plist-get report :owner-ids)))
|
|
(should (= (plist-get report :created-objects) 0))
|
|
(should (= (plist-get report :removed-objects) 0))
|
|
(with-current-buffer buffer
|
|
(should
|
|
(equal-including-properties
|
|
(buffer-substring (point-min) (point-max))
|
|
(ebox-render (ebox--buffer-root-node buffer)))))))
|
|
(let ((candidate (ebox-candidate-begin buffer)))
|
|
(ebox-candidate-replace-host-ref
|
|
candidate 'left
|
|
(ebox-create :key 'left :host-ref 'left
|
|
:content "left-old" :font 'bold))
|
|
(ebox-candidate-replace-host-ref
|
|
candidate 'right
|
|
(ebox-create :key 'right :host-ref 'right
|
|
:content "right-old"))
|
|
(ebox-candidate-replace-host-ref
|
|
candidate 'paint-a
|
|
(ebox-create :key 'paint-a :host-ref 'paint-a
|
|
:content "paint-a" :color "#111111"))
|
|
(ebox-candidate-replace-host-ref
|
|
candidate 'paint-b
|
|
(ebox-create :key 'paint-b :host-ref 'paint-b
|
|
:content "paint-b" :bgcolor "#222222"))
|
|
(ebox-candidate-replace-host-ref
|
|
candidate 'paint-c
|
|
(ebox-create :key 'paint-c :host-ref 'paint-c
|
|
:content "paint-c" :color "#333333"))
|
|
(let ((report (ebox-commit buffer candidate)))
|
|
(should (eq (plist-get report :projection-kind)
|
|
'mixed-owner-reflow))
|
|
(with-current-buffer buffer
|
|
(should
|
|
(equal-including-properties
|
|
(buffer-substring (point-min) (point-max))
|
|
(ebox-render (ebox--buffer-root-node buffer))))))))
|
|
(when (buffer-live-p buffer) (kill-buffer buffer)))))
|
|
|
|
(ert-deftest ebox-commit-mixed-owner-reflow-rolls-back-and-retries ()
|
|
"Mixed publication restores the old pair after final-accept failure."
|
|
(let* ((buffer
|
|
(ebox-render-to-buffer
|
|
(generate-new-buffer-name " *ebox-mixed-owner-rollback* ")
|
|
(ebox-commit-test--mixed-owner-root
|
|
"left-old" "right-old" "#111111" "#222222" "#333333")))
|
|
(old-buffer (ebox-commit-test--buffer-string buffer))
|
|
(candidate
|
|
(ebox-commit-test--mixed-owner-candidate
|
|
buffer "left-new" "right-new" "#AAAAAA" "#BBBBBB" "#CCCCCC")))
|
|
(unwind-protect
|
|
(progn
|
|
(cl-letf (((symbol-function 'accept-change-group)
|
|
(lambda (_group) (error "mixed final accept"))))
|
|
(should-error (ebox-commit buffer candidate)))
|
|
(should (equal-including-properties
|
|
(ebox-commit-test--buffer-string buffer) old-buffer))
|
|
(let ((retry
|
|
(ebox-commit-test--mixed-owner-candidate
|
|
buffer "left-new" "right-new"
|
|
"#AAAAAA" "#BBBBBB" "#CCCCCC")))
|
|
(let ((report (ebox-commit buffer retry)))
|
|
(should (eq (plist-get report :projection-kind)
|
|
'mixed-owner-reflow))
|
|
(with-current-buffer buffer
|
|
(should
|
|
(equal-including-properties
|
|
(buffer-substring (point-min) (point-max))
|
|
(ebox-render (ebox--buffer-root-node buffer))))))))
|
|
(when (buffer-live-p buffer) (kill-buffer buffer)))))
|
|
|
|
(ert-deftest ebox-commit-mixed-owner-reflow-reuses-unchanged-fragments ()
|
|
"Delegate affected face composition to TP and retain fragment topology."
|
|
(let* ((buffer
|
|
(ebox-render-to-buffer
|
|
(generate-new-buffer-name " *ebox-mixed-owner-reuse* ")
|
|
(ebox-commit-test--mixed-owner-root
|
|
"left-old" "right-old" "#111111" "#222222" "#333333")))
|
|
(surface (with-current-buffer buffer ebox-surface--buffer-surface))
|
|
(old-state (tp-surface-client-state surface))
|
|
(fragment-count (length (plist-get old-state :surface-fragments)))
|
|
(repaints 0)
|
|
(metadata-calls 0)
|
|
(original
|
|
(symbol-function 'ebox-surface--repaint-output-fragment!))
|
|
(original-metadata
|
|
(symbol-function 'ebox-surface--fragment-metadata)))
|
|
(unwind-protect
|
|
(progn
|
|
(should
|
|
(cl-every
|
|
(lambda (fragment)
|
|
(or (null (plist-get fragment :paint-role-ids))
|
|
(plist-get fragment :paint-node-chain)))
|
|
(plist-get old-state :surface-fragments)))
|
|
(should (plist-get old-state :surface-owned-ranges))
|
|
(cl-letf (((symbol-function 'ebox-surface--repaint-output-fragment!)
|
|
(lambda (&rest args)
|
|
(cl-incf repaints)
|
|
(apply original args)))
|
|
((symbol-function 'ebox-surface--fragment-metadata)
|
|
(lambda (&rest args)
|
|
(cl-incf metadata-calls)
|
|
(apply original-metadata args))))
|
|
(should (eq (plist-get
|
|
(ebox-commit
|
|
buffer
|
|
(ebox-commit-test--mixed-owner-candidate
|
|
buffer "left-new" "right-new"
|
|
"#AAAAAA" "#BBBBBB" "#CCCCCC"))
|
|
:projection-kind)
|
|
'mixed-owner-reflow)))
|
|
(should (> fragment-count 0))
|
|
(should (zerop repaints))
|
|
(should
|
|
(plist-get (tp-surface-client-state surface)
|
|
:paint-property-contributions))
|
|
(let ((fragments
|
|
(plist-get (tp-surface-client-state surface)
|
|
:surface-fragments))
|
|
(offset 0))
|
|
(should fragments)
|
|
(dolist (fragment fragments)
|
|
(should (= offset (plist-get fragment :start)))
|
|
(setq offset (plist-get fragment :end)))
|
|
(should (= offset
|
|
(with-current-buffer buffer (buffer-size)))))
|
|
(should (= metadata-calls 0)))
|
|
(when (buffer-live-p buffer) (kill-buffer buffer)))))
|
|
|
|
(ert-deftest ebox-surface-owned-range-index-rebases-exact-boundaries ()
|
|
"Rebase retained ownership exactly and reject ambiguous inner boundaries."
|
|
(let* ((patches '((:old-start 5 :old-end 10
|
|
:new-start 5 :new-end 12)))
|
|
(ranges '((:object before :start 0 :end 5 :tags (:before t))
|
|
(:object changed :start 5 :end 10 :tags (:changed t))
|
|
(:object parent :start 0 :end 20 :tags (:parent t))
|
|
(:object after :start 10 :end 20 :tags (:after t))))
|
|
(rebased (cdr (ebox-surface--rebase-owned-ranges
|
|
ranges patches 22))))
|
|
(should
|
|
(equal (mapcar (lambda (range)
|
|
(list (plist-get range :object)
|
|
(plist-get range :start)
|
|
(plist-get range :end)))
|
|
rebased)
|
|
'((before 0 5) (changed 5 12) (parent 0 22) (after 12 22))))
|
|
(should-not
|
|
(ebox-surface--rebase-owned-ranges
|
|
'((:object ambiguous :start 6 :end 9)) patches 22))))
|
|
|
|
(ert-deftest ebox-commit-structure-skips-inapplicable-paint-span-proofs ()
|
|
"A structural transaction must not run proofs whose domain excludes it."
|
|
(let* ((buffer
|
|
(ebox-render-to-buffer
|
|
(generate-new-buffer-name " *ebox-structure-proof-domain* ")
|
|
(ebox-column
|
|
(ebox-create
|
|
:key 'target :host-ref 'target
|
|
:ebox-content-node
|
|
(ebox-column (ebox-create :key 'first :content "first"))))))
|
|
(span-calls 0)
|
|
(mixed-calls 0)
|
|
(old-span
|
|
(symbol-function 'ebox-incremental--span-patch-projection-proof))
|
|
(old-mixed
|
|
(symbol-function 'ebox-incremental--mixed-owner-proof)))
|
|
(unwind-protect
|
|
(let ((candidate (ebox-candidate-begin buffer)))
|
|
(ebox-candidate-replace-host-ref
|
|
candidate 'target
|
|
(ebox-create
|
|
:key 'target :host-ref 'target
|
|
:ebox-content-node
|
|
(ebox-column (ebox-create :key 'first :content "first")
|
|
(ebox-create :key 'second :content "second"))))
|
|
(cl-letf
|
|
(((symbol-function 'ebox-incremental--span-patch-projection-proof)
|
|
(lambda (&rest arguments)
|
|
(cl-incf span-calls)
|
|
(apply old-span arguments)))
|
|
((symbol-function 'ebox-incremental--mixed-owner-proof)
|
|
(lambda (&rest arguments)
|
|
(cl-incf mixed-calls)
|
|
(apply old-mixed arguments))))
|
|
(ebox-commit buffer candidate))
|
|
(should (zerop span-calls))
|
|
(should (zerop mixed-calls))
|
|
(should (string-match-p "second"
|
|
(ebox-commit-test--buffer-string buffer))))
|
|
(when (buffer-live-p buffer) (kill-buffer buffer)))))
|
|
|
|
(ert-deftest ebox-candidate-range-structure-stops-at-range-parent ()
|
|
"A Range child identity change must not mark copied ancestors structural."
|
|
(let* ((buffer
|
|
(ebox-render-to-buffer
|
|
(generate-new-buffer-name " *ebox-range-dirty-boundary* ")
|
|
(ebox-column
|
|
(ebox-child-range
|
|
'rows (ebox-create :key 'old :content "old")))))
|
|
(state (ebox--buffer-render-state buffer))
|
|
(parent-id
|
|
(plist-get (gethash 'rows (plist-get state :range-ref-table))
|
|
:parent-node-id))
|
|
captured)
|
|
(unwind-protect
|
|
(let ((candidate (ebox-candidate-begin buffer))
|
|
(original
|
|
(symbol-function 'ebox-incremental--surface-commit-input)))
|
|
(ebox-candidate-replace-range-ref
|
|
candidate 'rows (list (ebox-create :key 'new :content "new")))
|
|
(cl-letf
|
|
(((symbol-function 'ebox-incremental--surface-commit-input)
|
|
(lambda (target old-state prepared)
|
|
(setq captured (copy-tree (plist-get prepared :dirty-set)))
|
|
(funcall original target old-state prepared))))
|
|
(ebox-commit buffer candidate))
|
|
(should
|
|
(equal
|
|
(mapcar (lambda (entry)
|
|
(list (plist-get entry :node-id)
|
|
(plist-get entry :dirty-kind)
|
|
(plist-get entry :changed-keys)))
|
|
captured)
|
|
(list (list parent-id 'structure '(:children)))))
|
|
(should (string-match-p "new"
|
|
(ebox-commit-test--buffer-string buffer))))
|
|
(when (buffer-live-p buffer) (kill-buffer buffer)))))
|
|
|
|
(ert-deftest ebox-commit-mixed-owner-reflow-skips-full-owner-planner ()
|
|
"A proven mixed union should not also plan the complete dirty set."
|
|
(let* ((buffer
|
|
(ebox-render-to-buffer
|
|
(generate-new-buffer-name " *ebox-mixed-owner-plan* ")
|
|
(ebox-commit-test--mixed-owner-root
|
|
"left-old" "right-old" "#111111" "#222222" "#333333")))
|
|
(calls 0)
|
|
(axis-scans 0)
|
|
(original
|
|
(symbol-function 'ebox-incremental--layout-owner-plan))
|
|
(original-context-axes
|
|
(symbol-function 'ebox-surface--context-axes)))
|
|
(unwind-protect
|
|
(cl-letf
|
|
(((symbol-function 'ebox-incremental--layout-owner-plan)
|
|
(lambda (&rest arguments)
|
|
(cl-incf calls)
|
|
(apply original arguments)))
|
|
((symbol-function 'ebox-surface--context-axes)
|
|
(lambda (&rest arguments)
|
|
(cl-incf axis-scans)
|
|
(apply original-context-axes arguments))))
|
|
(let* ((report
|
|
(ebox-commit
|
|
buffer
|
|
(ebox-commit-test--mixed-owner-candidate
|
|
buffer "left-new" "right-new"
|
|
"#AAAAAA" "#BBBBBB" "#CCCCCC")))
|
|
(owner-ids (plist-get report :owner-ids)))
|
|
(should (eq (plist-get report :projection-kind)
|
|
'mixed-owner-reflow))
|
|
(should (= calls 0))
|
|
(should (= axis-scans 0))
|
|
(should (= (plist-get report :tp-scope-count)
|
|
(length owner-ids)))
|
|
(should-not (plist-get report :tp-scope-fallback))
|
|
(with-current-buffer buffer
|
|
(should
|
|
(equal-including-properties
|
|
(buffer-substring (point-min) (point-max))
|
|
(ebox-render (ebox--buffer-root-node buffer)))))))
|
|
(when (buffer-live-p buffer) (kill-buffer buffer)))))
|
|
|
|
(defun ebox-commit-test--allocation-closure-root
|
|
(toast paint-a paint-b &optional width footer-overflow root-overflow)
|
|
"Return a generic whole-line Flex allocation-closure fixture."
|
|
(ebox-create
|
|
:key 'root :width (list (or width 800))
|
|
:height (and root-overflow 1) :overflow root-overflow
|
|
:ebox-content-node
|
|
(ebox-column
|
|
(ebox-create :key 'status :host-ref 'status
|
|
:content "Theme: Light" :width '(200))
|
|
(ebox-create :key 'paint-a :host-ref 'paint-a
|
|
:content "paint-a" :color paint-a)
|
|
(ebox-create :key 'paint-b :host-ref 'paint-b
|
|
:content "paint-b" :bgcolor paint-b)
|
|
(ebox-create
|
|
:key 'footer-owner :width (list (or width 800))
|
|
:ebox-content-node
|
|
(ebox-column
|
|
(ebox-flex
|
|
:key 'footer :width (list (or width 800)) :flex-wrap 'wrap
|
|
:overflow footer-overflow :gap '(1 (4))
|
|
(ebox-create
|
|
:key 'toast-slot :width 'stretch :min-width 0
|
|
:flex-grow 1 :flex-shrink 1 :flex-basis '(0)
|
|
:ebox-content-node
|
|
(ebox-column
|
|
(ebox-create :key 'toast :host-ref 'toast :content toast)))
|
|
(ebox-create :key 'peer :content "database.sqlite")))))))
|
|
|
|
(defun ebox-commit-test--allocation-closure-candidate
|
|
(buffer toast paint-a paint-b)
|
|
"Return BUFFER candidate changing one Flex content and two paints."
|
|
(let ((candidate (ebox-candidate-begin buffer)))
|
|
(ebox-candidate-replace-host-ref
|
|
candidate 'toast
|
|
(ebox-create :key 'toast :host-ref 'toast :content toast))
|
|
(ebox-candidate-replace-host-ref
|
|
candidate 'paint-a
|
|
(ebox-create :key 'paint-a :host-ref 'paint-a
|
|
:content "paint-a" :color paint-a))
|
|
(ebox-candidate-replace-host-ref
|
|
candidate 'paint-b
|
|
(ebox-create :key 'paint-b :host-ref 'paint-b
|
|
:content "paint-b" :bgcolor paint-b))
|
|
candidate))
|
|
|
|
(defun ebox-commit-test--two-geometry-allocation-candidate
|
|
(buffer status toast paint-a paint-b)
|
|
"Return BUFFER candidate with one span and one allocation geometry owner."
|
|
(let ((candidate
|
|
(ebox-commit-test--allocation-closure-candidate
|
|
buffer toast paint-a paint-b)))
|
|
(ebox-candidate-replace-host-ref
|
|
candidate 'status
|
|
(ebox-create :key 'status :host-ref 'status
|
|
:content status :width '(200)))
|
|
candidate))
|
|
|
|
(ert-deftest ebox-commit-allocation-closure-round-trips-exactly ()
|
|
"Stable Flex allocation closes one content damage plus external paint."
|
|
(let* ((buffer
|
|
(ebox-render-to-buffer
|
|
(generate-new-buffer-name " *ebox-allocation-closure* ")
|
|
(ebox-commit-test--allocation-closure-root
|
|
"Light" "#111111" "#222222")))
|
|
(surface (with-current-buffer buffer ebox-surface--buffer-surface))
|
|
(full-renders 0)
|
|
(original-full-render
|
|
(symbol-function 'ebox-surface--render-candidate)))
|
|
(unwind-protect
|
|
(cl-letf (((symbol-function 'ebox-surface--render-candidate)
|
|
(lambda (&rest arguments)
|
|
(cl-incf full-renders)
|
|
(apply original-full-render arguments))))
|
|
(dolist (values '(("A longer dark notification"
|
|
"#AAAAAA" "#BBBBBB")
|
|
("Light" "#111111" "#222222")))
|
|
(let* ((before (tp-surface-revision surface))
|
|
(candidate
|
|
(apply #'ebox-commit-test--allocation-closure-candidate
|
|
buffer values))
|
|
(report (ebox-commit buffer candidate))
|
|
(state (tp-surface-client-state surface))
|
|
(owner-proof
|
|
(car (plist-get
|
|
(plist-get (plist-get state :mixed-owner-proof)
|
|
:geometry-proof)
|
|
:owner-proofs))))
|
|
(should (eq (plist-get report :projection-kind)
|
|
'mixed-owner-reflow))
|
|
(should (plist-get owner-proof :allocation-closure-p))
|
|
(should (= (tp-surface-revision surface) (1+ before)))
|
|
(should-not (plist-get report :tp-full-root))
|
|
(should-not (plist-get report :tp-scope-fallback))
|
|
(should (= (plist-get report :created-objects) 0))
|
|
(should (= (plist-get report :removed-objects) 0))
|
|
(should (= (or (plist-get report :moved-objects) 0) 0))
|
|
(with-current-buffer buffer
|
|
(should
|
|
(equal-including-properties
|
|
(buffer-substring (point-min) (point-max))
|
|
(ebox-render (ebox--buffer-root-node buffer)))))))
|
|
(should (= full-renders 0)))
|
|
(when (buffer-live-p buffer) (kill-buffer buffer)))))
|
|
|
|
(ert-deftest ebox-commit-allocation-closure-rolls-back-and-retries ()
|
|
"Failed allocation-closure publication restores state before retry."
|
|
(let* ((buffer
|
|
(ebox-render-to-buffer
|
|
(generate-new-buffer-name " *ebox-allocation-rollback* ")
|
|
(ebox-commit-test--allocation-closure-root
|
|
"Light" "#111111" "#222222")))
|
|
(surface (with-current-buffer buffer ebox-surface--buffer-surface))
|
|
(state (tp-surface-client-state surface))
|
|
(revision (tp-surface-revision surface))
|
|
(text (ebox-commit-test--buffer-string buffer)))
|
|
(unwind-protect
|
|
(progn
|
|
(cl-letf (((symbol-function 'accept-change-group)
|
|
(lambda (_group) (error "allocation final accept"))))
|
|
(should-error
|
|
(ebox-commit
|
|
buffer
|
|
(ebox-commit-test--allocation-closure-candidate
|
|
buffer "A longer dark notification" "#AAAAAA" "#BBBBBB"))))
|
|
(should (= (tp-surface-revision surface) revision))
|
|
(should (eq (tp-surface-client-state surface) state))
|
|
(should (equal-including-properties
|
|
(ebox-commit-test--buffer-string buffer) text))
|
|
(should (eq (plist-get
|
|
(ebox-commit
|
|
buffer
|
|
(ebox-commit-test--allocation-closure-candidate
|
|
buffer "A longer dark notification"
|
|
"#AAAAAA" "#BBBBBB"))
|
|
:projection-kind)
|
|
'mixed-owner-reflow)))
|
|
(when (buffer-live-p buffer) (kill-buffer buffer)))))
|
|
|
|
(ert-deftest ebox-commit-two-geometry-allocation-closure-round-trips ()
|
|
"Combine one ordinary span with one allocation closure after global miss."
|
|
(let* ((buffer
|
|
(ebox-render-to-buffer
|
|
(generate-new-buffer-name " *ebox-two-geometry-closure* ")
|
|
(ebox-commit-test--allocation-closure-root
|
|
"Light" "#111111" "#222222")))
|
|
(surface (with-current-buffer buffer ebox-surface--buffer-surface))
|
|
(full-renders 0)
|
|
(original-full-render
|
|
(symbol-function 'ebox-surface--render-candidate)))
|
|
(unwind-protect
|
|
(cl-letf (((symbol-function 'ebox-surface--render-candidate)
|
|
(lambda (&rest arguments)
|
|
(cl-incf full-renders)
|
|
(apply original-full-render arguments))))
|
|
(dolist (values '(("Theme: Dark" "A longer dark notification"
|
|
"#AAAAAA" "#BBBBBB")
|
|
("Theme: Light" "Light"
|
|
"#111111" "#222222")))
|
|
(let* ((before (tp-surface-revision surface))
|
|
(report
|
|
(ebox-commit
|
|
buffer
|
|
(apply
|
|
#'ebox-commit-test--two-geometry-allocation-candidate
|
|
buffer values)))
|
|
(state (tp-surface-client-state surface))
|
|
(proofs
|
|
(plist-get
|
|
(plist-get (plist-get state :mixed-owner-proof)
|
|
:geometry-proof)
|
|
:owner-proofs)))
|
|
(should (eq (plist-get report :projection-kind)
|
|
'mixed-owner-reflow))
|
|
(should (= 2 (length proofs)))
|
|
(should (cl-some (lambda (proof)
|
|
(plist-get proof :allocation-closure-p))
|
|
proofs))
|
|
(should (= (tp-surface-revision surface) (1+ before)))
|
|
(should-not (plist-get report :tp-full-root))
|
|
(should-not (plist-get report :tp-scope-fallback))
|
|
(with-current-buffer buffer
|
|
(should
|
|
(equal-including-properties
|
|
(buffer-substring (point-min) (point-max))
|
|
(ebox-render (ebox--buffer-root-node buffer)))))))
|
|
(should (= full-renders 0)))
|
|
(when (buffer-live-p buffer) (kill-buffer buffer)))))
|
|
|
|
(ert-deftest ebox-allocation-closure-allows-recomposable-ancestor-paint ()
|
|
"Allow ancestor paint but reject paint at/below an allocation owner."
|
|
(let ((parents (make-hash-table :test #'eql)))
|
|
;; 1(root) -> 2(paint ancestor) -> 3(geometry) -> 4(paint descendant)
|
|
(puthash 2 1 parents)
|
|
(puthash 3 2 parents)
|
|
(puthash 4 3 parents)
|
|
(let ((state (list :parent-table parents)))
|
|
(should
|
|
(ebox-incremental--allocation-closure-paint-disjoint-p
|
|
state '(3) '(2)))
|
|
(should-not
|
|
(ebox-incremental--allocation-closure-paint-disjoint-p
|
|
state '(3) '(3)))
|
|
(should-not
|
|
(ebox-incremental--allocation-closure-paint-disjoint-p
|
|
state '(3) '(4))))))
|
|
|
|
(ert-deftest ebox-commit-allocation-closure-unsafe-shapes-fallback ()
|
|
"Wrap/width, overflow, and scroll changes cannot use allocation closure."
|
|
(dolist (case '((120 nil nil) (800 visible nil) (800 nil scroll)))
|
|
(pcase-let ((`(,width ,footer-overflow ,root-overflow) case))
|
|
(let ((buffer
|
|
(ebox-render-to-buffer
|
|
(generate-new-buffer-name " *ebox-allocation-fallback* ")
|
|
(ebox-commit-test--allocation-closure-root
|
|
"Light" "#111111" "#222222"
|
|
width footer-overflow root-overflow))))
|
|
(unwind-protect
|
|
(let ((report
|
|
(ebox-commit
|
|
buffer
|
|
(ebox-commit-test--allocation-closure-candidate
|
|
buffer (make-string 200 ?X) "#AAAAAA" "#BBBBBB"))))
|
|
(should-not (eq (plist-get report :projection-kind)
|
|
'mixed-owner-reflow)))
|
|
(when (buffer-live-p buffer) (kill-buffer buffer)))))))
|
|
|
|
(ert-deftest ebox-commit-allocation-closure-proof-misses-fallback ()
|
|
"Topology, selector, cascade, and role misses reject allocation closure."
|
|
(dolist (kind '(topology selector cascade))
|
|
(let ((buffer
|
|
(ebox-render-to-buffer
|
|
(generate-new-buffer-name " *ebox-allocation-proof-miss* ")
|
|
(ebox-commit-test--allocation-closure-root
|
|
"Light" "#111111" "#222222"))))
|
|
(unwind-protect
|
|
(let ((candidate (ebox-candidate-begin buffer)))
|
|
(ebox-candidate-replace-host-ref
|
|
candidate 'toast
|
|
(if (eq kind 'topology)
|
|
(ebox-create
|
|
:key 'toast :host-ref 'toast
|
|
:ebox-content-node
|
|
(ebox-column
|
|
(ebox-create :key 'nested-toast
|
|
:content "A longer notification")))
|
|
(ebox-create :key 'toast :host-ref 'toast
|
|
:class (and (eq kind 'selector) "changed")
|
|
:content "A longer notification")))
|
|
(ebox-candidate-replace-host-ref
|
|
candidate 'paint-a
|
|
(ebox-create :key 'paint-a :host-ref 'paint-a
|
|
:content "paint-a" :color "#AAAAAA"))
|
|
(let ((report
|
|
(if (eq kind 'cascade)
|
|
(cl-letf
|
|
(((symbol-function 'ebox-style-cascade-active-p)
|
|
(lambda () t))
|
|
((symbol-function
|
|
'ebox-surface--cascade-local-owner-proof-p)
|
|
(lambda (&rest _) nil)))
|
|
(ebox-commit buffer candidate))
|
|
(ebox-commit buffer candidate))))
|
|
(should-not (eq (plist-get report :projection-kind)
|
|
'mixed-owner-reflow))))
|
|
(when (buffer-live-p buffer) (kill-buffer buffer)))))
|
|
(let* ((buffer
|
|
(ebox-render-to-buffer
|
|
(generate-new-buffer-name " *ebox-allocation-role-miss* ")
|
|
(ebox-commit-test--allocation-closure-root
|
|
"Light" "#111111" "#222222")))
|
|
(original-output
|
|
(symbol-function 'ebox-surface--mixed-owner-output))
|
|
mixed-output)
|
|
(unwind-protect
|
|
(progn
|
|
(ebox--refresh-buffer-layout-snapshots buffer t)
|
|
(cl-letf
|
|
(((symbol-function
|
|
'ebox-surface--rendered-role-topology-signature)
|
|
(lambda (&rest _) '(:roles (mismatched))))
|
|
((symbol-function 'ebox-surface--mixed-owner-output)
|
|
(lambda (&rest arguments)
|
|
(setq mixed-output (apply original-output arguments)))))
|
|
(ebox-commit
|
|
buffer
|
|
(ebox-commit-test--allocation-closure-candidate
|
|
buffer "A longer notification" "#AAAAAA" "#BBBBBB"))
|
|
(should-not mixed-output)))
|
|
(when (buffer-live-p buffer) (kill-buffer buffer)))))
|
|
|
|
(provide 'ebox-commit-tests)
|
|
|
|
;;; ebox-commit-tests.el ends here
|