ebox/tests/ebox-surface-tests.el
Kinneyzhang fad54d7fb1 feat(ebox): project layouts into TP surface plans
Assign retained TP identity before layout and emit pure, runtime-free surface plans with exact character and text-property equivalence. Keep live publication unchanged for the staged cutover and add focused surface, package, docs, and CI contracts.\n\nVerified: make check EMACS=/Applications/Emacs.app/Contents/MacOS/Emacs\nVerified: WERROR byte compilation for all 16 active Lisp files\nVerified: focused ebox-surface checkdoc has zero warnings
2026-08-06 04:13:13 +08:00

295 lines
13 KiB
EmacsLisp

;;; ebox-surface-tests.el --- TP surface projection tests -*- lexical-binding: t; -*-
(require 'cl-lib)
(require 'ert)
(unless load-file-name
(error "This test file must be loaded from disk, not eval'ed directly"))
(setq load-prefer-newer t)
(load-file (expand-file-name "../ebox.el"
(file-name-directory load-file-name)))
(require 'tp-surface)
(defun ebox-surface-test--reset-render-state ()
"Reset render identities and side tables used by projection tests."
(setq ebox--region-id-counter 0
ebox--runtime-node-id-counter 0)
(dolist (table (list ebox--region-box-table
ebox--scroll-global-state
ebox--rendered-root-metadata-table))
(clrhash table)))
(defun ebox-surface-test--interactive-content ()
"Return fresh interactive propertized content for projection tests."
(let ((map (make-sparse-keymap)))
(define-key map [mouse-1] #'ignore)
(propertize "Open" 'keymap map 'mouse-face 'highlight
'help-echo "Open this item")))
(defun ebox-surface-test--fixtures ()
"Return named fresh layout builders covering active Ebox layout kinds."
(list
(cons 'box
(lambda ()
(ebox-create :key 'box :content (ebox-surface-test--interactive-content)
:width '(120) :padding '(1 (4))
:border "#334155" :bgcolor "#E2E8F0"
:color "#0F172A")))
(cons 'row-column
(lambda ()
(ebox-column
(ebox-row
(ebox-create :key 'left :content "Left" :width '(70)
:bgcolor "#DBEAFE" :color "#172554")
(ebox-create :key 'right :content "Right\nDetail" :width '(90)
:bgcolor "#DCFCE7" :color "#14532D"))
(ebox-create :key 'footer :content "Footer" :width '(160)
:bgcolor "#F1F5F9" :color "#0F172A"))))
(cons 'flex
(lambda ()
(ebox-flex
:width '(210) :flex-wrap 'wrap :column-gap '(10)
(ebox-flex-item
(ebox-create :key 'grow :content "Grow" :width '(80)
:bgcolor "#EDE9FE" :color "#2E1065")
:flex-grow 1 :flex-basis '(80))
(ebox-flex-item
(ebox-create :key 'fixed :content "Fixed" :width '(120)
:bgcolor "#FFEDD5" :color "#7C2D12")))))
(cons 'grid
(lambda ()
(ebox-grid
:width '(220) :grid-template-columns '((70) 1fr)
:grid-template-rows '(2) :gap '(1 (8))
:border "#475569"
(ebox-create :key 'grid-left :content "A\nAA"
:bgcolor "#E0F2FE" :color "#0C4A6E")
(ebox-create :key 'grid-right :content "B\nBB"
:bgcolor "#FCE7F3" :color "#831843"))))
(cons 'overflow-scroll
(lambda ()
(ebox-create :key 'scroll :content "zero\none\ntwo\nthree"
:width '(100) :height 2 :overflow 'scroll
:bgcolor "#1E293B" :color "#F8FAFC")))))
(defun ebox-surface-test--render-fresh (builder projector-p)
"Render BUILDER after a reset, using the TP projector when PROJECTOR-P."
(ebox-surface-test--reset-render-state)
(let ((ebox-viewport-width 240)
(ebox-viewport-height 12)
(node (funcall builder)))
(if projector-p
(tp-surface-materialize-string
(ebox-surface--producer node))
(ebox-render node))))
(defun ebox-surface-test--walk-runtime (node function)
"Call FUNCTION for every runtime NODE in preorder."
(funcall function node)
(dolist (child (ebox-tree-node-children node))
(ebox-surface-test--walk-runtime child function)))
(defun ebox-surface-test--plan-runtime-value-p (value)
"Return non-nil when VALUE is forbidden runtime state in a pure plan."
(cond
((or (markerp value) (bufferp value) (tp-object-p value)
(tp-binding-p value) (tp-surface-p value))
t)
((consp value)
(or (ebox-surface-test--plan-runtime-value-p (car value))
(ebox-surface-test--plan-runtime-value-p (cdr value))))
((vectorp value)
(cl-some #'ebox-surface-test--plan-runtime-value-p value))
(t nil)))
(defun ebox-surface-test--plan-pure-p (plan)
"Return non-nil when PLAN contains only pure projection data."
(and
(not (cl-some
#'ebox-surface-test--plan-runtime-value-p
(list (tp-surface-plan-key plan)
(tp-surface-plan-kind plan)
(tp-surface-plan-text plan)
(tp-surface-plan-props plan)
(tp-surface-plan-tags plan))))
(cl-every #'ebox-surface-test--plan-pure-p
(tp-surface-plan-children plan))))
(defun ebox-surface-test--object-by-key (state key)
"Return the candidate surface object for Ebox node KEY in STATE."
(let (object)
(maphash
(lambda (_node-id node)
(when (equal (plist-get node :key) key)
(setq object (plist-get node :surface-object))))
(plist-get state :node-table))
object))
(ert-deftest ebox-surface-projects-every-layout-with-exact-equivalence ()
"TP projection should preserve every character and text property interval."
(dolist (fixture (ebox-surface-test--fixtures))
(let* ((builder (cdr fixture))
(expected (ebox-surface-test--render-fresh builder nil))
(actual (ebox-surface-test--render-fresh builder t)))
(should (equal-including-properties actual expected))
(should (equal (mapcar #'ebox--string-pixel-width
(ebox-string-lines actual))
(mapcar #'ebox--string-pixel-width
(ebox-string-lines expected)))))))
(ert-deftest ebox-surface-assigns-object-identity-before-layout ()
"Every candidate runtime node should own a TP object before layout starts."
(let ((original (symbol-function 'ebox-render))
checked captured)
(cl-letf (((symbol-function 'ebox-render)
(lambda (node)
(unless checked
(setq checked t)
(ebox-surface-test--walk-runtime
node
(lambda (runtime-node)
(let ((object (plist-get runtime-node :surface-object)))
(should (tp-object-p object))
(push object captured)))))
(funcall original node))))
(ebox-surface-test--render-fresh
(cdr (assq 'grid (ebox-surface-test--fixtures))) t))
(should checked)
(should captured)
(dolist (object captured)
(should-not (tp-object-live-p object)))))
(ert-deftest ebox-surface-plan-stays-free-of-runtime-state ()
"The generic surface plan should not contain markers or runtime handles."
(let ((original (symbol-function 'tp-surface-result-create))
captured)
(cl-letf (((symbol-function 'tp-surface-result-create)
(lambda (plan &optional client-state)
(setq captured plan)
(funcall original plan client-state))))
(ebox-surface-test--render-fresh
(cdr (assq 'box (ebox-surface-test--fixtures))) t))
(should (tp-surface-plan-p captured))
(should (ebox-surface-test--plan-pure-p captured))))
(ert-deftest ebox-surface-projection-does-not-mutate-a-buffer ()
"Pure projection should not call any final buffer mutation primitive."
(cl-letf (((symbol-function 'insert)
(lambda (&rest _) (error "Unexpected buffer insertion")))
((symbol-function 'erase-buffer)
(lambda (&rest _) (error "Unexpected buffer erase")))
((symbol-function 'delete-region)
(lambda (&rest _) (error "Unexpected buffer deletion")))
((symbol-function 'replace-region-contents)
(lambda (&rest _) (error "Unexpected buffer replacement"))))
(should
(stringp
(ebox-surface-test--render-fresh
(cdr (assq 'row-column (ebox-surface-test--fixtures))) t)))))
(ert-deftest ebox-surface-logical-box-owns-disjoint-render-fragments ()
"One logical Ebox box should resolve all of its separated painted regions."
(ebox-surface-test--reset-render-state)
(let ((buffer (generate-new-buffer " *ebox-surface-fragments*"))
surface)
(unwind-protect
(progn
(setq surface
(tp-surface-mount
buffer
(ebox-surface--producer
(funcall (cdr (assq 'box (ebox-surface-test--fixtures)))))
'(:capability content)))
(let* ((state (tp-surface-client-state surface))
(objects (plist-get state :region-surface-object-table))
logical)
(maphash (lambda (_region-id object)
(unless logical (setq logical object)))
objects)
(should (tp-object-live-p logical))
(should (> (length (tp-object-mounts logical)) 1))))
(when (and surface (tp-surface-live-p surface))
(tp-surface-unmount surface))
(when (buffer-live-p buffer)
(kill-buffer buffer)))))
(ert-deftest ebox-surface-reuses-one-source-with-isolated-runtime-state ()
"One source description should mount into two independent TP surfaces."
(ebox-surface-test--reset-render-state)
(let* ((source
(ebox-create :key 'shared :content "Shared" :width '(100)
:bgcolor "#E2E8F0" :color "#0F172A"))
(first-buffer (generate-new-buffer " *ebox-surface-first*"))
(second-buffer (generate-new-buffer " *ebox-surface-second*"))
first second)
(unwind-protect
(progn
(setq first
(tp-surface-mount
first-buffer (ebox-surface--producer source)
'(:capability content)))
(setq second
(tp-surface-mount
second-buffer (ebox-surface--producer source)
'(:capability content)))
(let* ((first-state (tp-surface-client-state first))
(second-state (tp-surface-client-state second))
(first-root (plist-get first-state :root-node))
(second-root (plist-get second-state :root-node)))
(should-not (eq first-root second-root))
(should-not (eq (plist-get first-root :surface-object)
(plist-get second-root :surface-object)))
(should-not (plist-member source :node-id))
(should-not (plist-member source :region-id))
(should-not (plist-member source :surface-object))))
(dolist (surface (list first second))
(when (and surface (tp-surface-live-p surface))
(tp-surface-unmount surface)))
(dolist (buffer (list first-buffer second-buffer))
(when (buffer-live-p buffer)
(kill-buffer buffer))))))
(ert-deftest ebox-surface-keyed-reorder-retains-logical-objects ()
"A keyed child reorder should retain TP objects through a new Ebox runtime."
(ebox-surface-test--reset-render-state)
(let ((buffer (generate-new-buffer " *ebox-surface-reorder*"))
surface)
(unwind-protect
(let* ((first-source
(ebox-row
(ebox-create :key 'left :content "Left" :width '(60))
(ebox-create :key 'right :content "Right" :width '(60))))
(_mount
(setq surface
(tp-surface-mount
buffer (ebox-surface--producer first-source)
'(:capability content))))
(first-state (tp-surface-client-state surface))
(left (ebox-surface-test--object-by-key first-state 'left))
(right (ebox-surface-test--object-by-key first-state 'right))
(next-source
(ebox-row
(ebox-create :key 'right :content "Right!" :width '(60))
(ebox-create :key 'left :content "Left!" :width '(60)))))
(tp-surface-update
surface (ebox-surface--producer next-source first-state))
(let ((next-state (tp-surface-client-state surface)))
(should (eq left
(ebox-surface-test--object-by-key next-state 'left)))
(should (eq right
(ebox-surface-test--object-by-key next-state 'right)))
(should (string-match-p
"Right!.*Left!"
(substring-no-properties
(with-current-buffer buffer (buffer-string)))))))
(when (and surface (tp-surface-live-p surface))
(tp-surface-unmount surface))
(when (buffer-live-p buffer)
(kill-buffer buffer)))))
(provide 'ebox-surface-tests)
;;; ebox-surface-tests.el ends here