Some checks are pending
CI / test (29.1) (push) Waiting to run
CI / test (30.2) (push) Waiting to run
CI / native-build (macos-latest) (push) Waiting to run
CI / native-build (ubuntu-latest) (push) Waiting to run
CI / native-build (windows-latest) (push) Waiting to run
CI / native-msrv (macos-latest) (push) Waiting to run
CI / native-msrv (ubuntu-latest) (push) Waiting to run
CI / native-msrv (windows-latest) (push) Waiting to run
Batch region callbacks and preserve native repeated-click behavior. Reuse proven layer slots, root scroll content and nested viewport allocations while retaining conservative fallbacks. Isolate cold prefix producers and caches before publication, retain current-owner continuations, and project separator ownership consistently. Add lifecycle, rollback, layout and performance regression coverage with bilingual contracts.
349 lines
19 KiB
EmacsLisp
349 lines
19 KiB
EmacsLisp
;;; ebox-layer-scroll-tests.el --- Retained layered root scroll -*- lexical-binding: t; -*-
|
|
|
|
;;; Commentary:
|
|
;; Cached root scroll projects composed content without repositioning its layers.
|
|
;; Nested owners and active root projections keep their composition boundary.
|
|
|
|
;;; Code:
|
|
|
|
(require 'ert)
|
|
(require 'cl-lib)
|
|
(require 'ebox)
|
|
|
|
(defmacro ebox-layer-scroll-test--with-buffer (&rest body)
|
|
"Run BODY in an isolated Elisp scroll buffer without native window scrolling."
|
|
(declare (indent 0) (debug t))
|
|
`(cl-letf (((symbol-function 'ebox-native-reflow-layout-ready-p)
|
|
(lambda () nil)))
|
|
(let ((ebox-viewport-width 24) (ebox-viewport-height 8)
|
|
(ebox-native-buffer-scroll nil)
|
|
(ebox-runtime-idle-prewarm nil)
|
|
(ebox-runtime-idle-reflow-cache-prewarm nil)
|
|
(ebox-scroll-lazy-idle-prefetch-lines 0))
|
|
(with-temp-buffer ,@body))))
|
|
|
|
(defun ebox-layer-scroll-test--input (&optional chrome portal nested)
|
|
"Build a layered scrolling root with CHROME, hidden PORTAL and NESTED owner."
|
|
(ebox-build
|
|
`(column :id "root" :width (ch 20) :height (lh 5) :overflow scroll
|
|
:padding ,(pcase chrome
|
|
('fractional '((lh 1) (px 1.5)))
|
|
('nil '((lh 0) (ch 0)))
|
|
(_ '((lh 1) (ch 1))))
|
|
:color "#102030" :background-color "#DDEEFF"
|
|
(box :id "before" "BEFORE")
|
|
(box :id "host" :height (lh 4)
|
|
(box :id "lower" "BASE0000\nBASE1111\nBASE2222\nBASE3333")
|
|
(box :id "upper" :position absolute :left (ch 2) :top (lh 1)
|
|
:width (ch 4) :height (lh 2) :background-color "#334455"
|
|
:color "#FFFFFF" :pointer hand :help-echo "layer action"
|
|
:keymap (keymap (13 . ignore)) "TOP1\nTOP2"))
|
|
(box :id "after" "AFTER000\nAFTER111\nAFTER222\nAFTER333")
|
|
,@(when nested
|
|
'((box :id "nested" :width (ch 10) :height (lh 2)
|
|
:overflow scroll :padding ((lh 1) (ch 1))
|
|
"NESTED0\nNESTED1\nNESTED2\nNESTED3")))
|
|
,@(when portal
|
|
'((box :id "portal-owner" :height (lh 1)
|
|
(box :id "portal" :position absolute :layer root
|
|
:anchor "before" :visibility hidden
|
|
:width (ch 6) :height (lh 1) "PORTAL")))))))
|
|
|
|
(defun ebox-layer-scroll-test--root-state ()
|
|
"Return the mounted root's current scroll state."
|
|
(let* ((runtime (ebox--buffer-render-state (current-buffer)))
|
|
(root (plist-get runtime :root-node)))
|
|
(gethash (plist-get root :region-id) (plist-get runtime :scroll-state-table))))
|
|
|
|
(defun ebox-layer-scroll-test--portal-chain-input ()
|
|
"Build two visible anchored root projections through a clipped logical owner."
|
|
(ebox-build
|
|
'(column :id "root" :width (ch 20) :height (lh 6) :overflow scroll
|
|
:padding ((lh 1) (px 1.5)) :margin-left (px 0.5)
|
|
:background-color "#EEF4F8" :color "#173047"
|
|
(box "PRE0\nPRE1\nPRE2")
|
|
(box :id "clipped-owner" :height (lh 1) :overflow hidden
|
|
(box :id "trigger" :position relative :left (ch 1)
|
|
:width (ch 6) :height (lh 1) "TRIGGER")
|
|
;; Source order is the reverse of the anchor dependency order.
|
|
(box :id "dependent" :position absolute :layer root
|
|
:anchor "provider-anchor" :placement bottom-end :z-index 9
|
|
:width (ch 5) :height (lh 1) :background-color "#704080"
|
|
:color "#FFFFFF" :pointer hand :help-echo "Dependent action"
|
|
:keymap (keymap (13 . ignore)) "CHAIN")
|
|
(box :id "provider" :position absolute :layer root :anchor "trigger"
|
|
:placement bottom-start :width (ch 7) :height (lh 2)
|
|
:background-color "#206050" :color "#FFFFFF"
|
|
(box :id "provider-anchor" :position relative :left (ch 1)
|
|
:width (ch 4) :height (lh 1) :help-echo "Provider action"
|
|
:keymap (keymap (13 . ignore)) "LINK")
|
|
(box "PANEL")))
|
|
(box "POST0\nPOST1\nPOST2\nPOST3\nPOST4\nPOST5\nPOST6\nPOST7"))))
|
|
|
|
(defun ebox-layer-scroll-test--assert-fresh ()
|
|
"Compare the committed window with an independent render at the same offset."
|
|
(let* ((runtime (ebox--buffer-render-state (current-buffer)))
|
|
(root (plist-get runtime :root-node))
|
|
(offset (ebox-get root :scroll-offset))
|
|
(input (plist-get (ebox-surface-buffer-snapshot (current-buffer)) :input))
|
|
(ebox-viewport-width (plist-get runtime :viewport-width))
|
|
(ebox-viewport-height (plist-get runtime :viewport-height))
|
|
(set-offset
|
|
(lambda (candidate)
|
|
(unless (eq candidate root)
|
|
(ebox-put candidate :scroll-offset offset)
|
|
(ebox-put candidate :ebox-scroll-offset-controlled-p t))))
|
|
actual fresh)
|
|
(setq actual (buffer-string))
|
|
;; Bind only the first layout invocation, leaving nested scroll offsets
|
|
;; to the committed source used by this root-only oracle.
|
|
(let ((render (symbol-function 'ebox--render-layout)) first)
|
|
(cl-letf (((symbol-function 'ebox--render-layout)
|
|
(lambda (candidate)
|
|
(unless first (setq first t) (funcall set-offset candidate))
|
|
(funcall render candidate))))
|
|
(setq fresh (ebox-render input))))
|
|
(should (equal (substring-no-properties actual) (substring-no-properties fresh)))
|
|
(should (= (length actual) (length fresh)))
|
|
(dotimes (position (length actual))
|
|
(dolist (property '(face font-lock-face display help-echo keymap mouse-face pointer))
|
|
(ert-info ((format "Property %S at %d" property position))
|
|
(should (equal (get-text-property position property actual)
|
|
(get-text-property position property fresh))))))))
|
|
|
|
(ert-deftest ebox-layer-scroll-root-reuses-composed-content ()
|
|
"Pure root scroll retains layered content and skips the complete tree render."
|
|
(dolist (chrome '(nil t fractional))
|
|
(ebox-layer-scroll-test--with-buffer
|
|
(ebox-render-to-buffer
|
|
(current-buffer) (ebox-layer-scroll-test--input chrome t))
|
|
(let* ((state (ebox-layer-scroll-test--root-state))
|
|
(region-id (plist-get (plist-get state :box) :region-id))
|
|
(content (plist-get state :content-lines))
|
|
(root-renders 0)
|
|
(render (symbol-function 'ebox-surface--render-candidate)))
|
|
(when (eq chrome 'fractional)
|
|
(should (plist-get (ebox--buffer-render-state (current-buffer))
|
|
:fractional-pixel-output-p)))
|
|
(dolist (delta '(1 1 -1 -1))
|
|
(cl-letf (((symbol-function 'ebox-surface--render-candidate)
|
|
(lambda (&rest arguments)
|
|
(cl-incf root-renders)
|
|
(apply render arguments))))
|
|
(should (= delta (ebox--scroll-region-by region-id delta 1)))
|
|
(should (eq content
|
|
(plist-get (ebox-layer-scroll-test--root-state) :content-lines)))
|
|
(should (eq (plist-get (ebox-buffer-update-report (current-buffer))
|
|
:projection-kind)
|
|
'scroll-patch)))
|
|
(ebox-layer-scroll-test--assert-fresh))
|
|
(should (= root-renders 0))))))
|
|
|
|
(ert-deftest ebox-layer-scroll-retains-input-facts-with-unready-nested-window ()
|
|
"A nested window's chrome cannot invalidate the root's style or axis facts."
|
|
(ebox-layer-scroll-test--with-buffer
|
|
(ebox-render-to-buffer
|
|
(current-buffer) (ebox-layer-scroll-test--input t nil t))
|
|
(let* ((runtime (ebox--buffer-render-state (current-buffer)))
|
|
(root-state (ebox-layer-scroll-test--root-state))
|
|
(region-id (plist-get (plist-get root-state :box) :region-id))
|
|
(axes (plist-get runtime :viewport-dependent-node-id-axes))
|
|
(nested (car (ebox-selector-query-buffer (current-buffer) "#nested")))
|
|
(nested-state (gethash (plist-get nested :region-id)
|
|
(plist-get runtime :scroll-state-table))))
|
|
(should nested-state)
|
|
(should-not (ebox--scroll-state-retained-window-ready-p nested-state))
|
|
(cl-letf (((symbol-function 'ebox-surface--context-axes)
|
|
(lambda (&rest _) (ert-fail "Scroll rescanned context axes")))
|
|
((symbol-function 'ebox-surface--inline-inheritance-required-p)
|
|
(lambda (&rest _) (ert-fail "Scroll rescanned inheritance")))
|
|
((symbol-function 'ebox-surface--apply-node-style)
|
|
(lambda (&rest _) (ert-fail "Scroll reapplied unchanged styles"))))
|
|
(should (= 1 (ebox--scroll-region-by region-id 1 1))))
|
|
(should (eq axes (plist-get (ebox--buffer-render-state (current-buffer))
|
|
:viewport-dependent-node-id-axes)))
|
|
(ebox-layer-scroll-test--assert-fresh))))
|
|
|
|
(ert-deftest ebox-layer-scroll-content-and-visibility-refresh-composed-cache ()
|
|
"Content and visibility updates replace the cache consumed by later scrolling."
|
|
(ebox-layer-scroll-test--with-buffer
|
|
(ebox-render-to-buffer
|
|
(current-buffer) (ebox-layer-scroll-test--input t))
|
|
(let* ((root-state (ebox-layer-scroll-test--root-state))
|
|
(region-id (plist-get (plist-get root-state :box) :region-id)))
|
|
(should (= 1 (ebox--scroll-region-by region-id 1 1)))
|
|
(dolist (change '((:content "NEW1\nNEW2")
|
|
(:visibility hidden) (:visibility visible)))
|
|
(let ((content (plist-get (ebox-layer-scroll-test--root-state) :content-lines)))
|
|
(apply #'ebox-region-update "upper" change)
|
|
(should-not (eq content (plist-get (ebox-layer-scroll-test--root-state)
|
|
:content-lines))))
|
|
(ebox-layer-scroll-test--assert-fresh)
|
|
(should (= 1 (ebox--scroll-region-by region-id 1 1)))
|
|
(ebox-layer-scroll-test--assert-fresh)
|
|
(should (= -1 (ebox--scroll-region-by region-id -1 1)))))))
|
|
|
|
(ert-deftest ebox-layer-scroll-rejected-publication-keeps-cache-and-input ()
|
|
"Both publication failure stages preserve composed cache and current offset."
|
|
(dolist (failure-step '(text client-state))
|
|
(ebox-layer-scroll-test--with-buffer
|
|
(ebox-render-to-buffer
|
|
(current-buffer) (ebox-layer-scroll-test--input 'fractional))
|
|
(let* ((runtime (ebox--buffer-render-state (current-buffer)))
|
|
(state (ebox-layer-scroll-test--root-state))
|
|
(region-id (plist-get (plist-get state :box) :region-id))
|
|
(content (plist-get state :content-lines))
|
|
(snapshot (ebox-surface-buffer-snapshot (current-buffer)))
|
|
(revision (ebox-surface-buffer-revision (current-buffer)))
|
|
(before (buffer-string)))
|
|
(let ((tp--surface-publication-step-function
|
|
(lambda (step _surface)
|
|
(when (eq step failure-step) (error "Reject cached root scroll")))))
|
|
(should-error (ebox--scroll-region-by region-id 1 1)))
|
|
(should (eq runtime (ebox--buffer-render-state (current-buffer))))
|
|
(should (eq state (ebox-layer-scroll-test--root-state)))
|
|
(should (eq content (plist-get state :content-lines)))
|
|
(let* ((after (ebox-surface-buffer-snapshot (current-buffer)))
|
|
(same-input
|
|
(equal (ebox-canonical-input-roots (plist-get snapshot :input))
|
|
(ebox-canonical-input-roots (plist-get after :input)))))
|
|
(should same-input)
|
|
(should (= (plist-get snapshot :mount-id) (plist-get after :mount-id))))
|
|
(should (= revision (ebox-surface-buffer-revision (current-buffer))))
|
|
(should (equal-including-properties before (buffer-string)))
|
|
(should (= 1 (ebox--scroll-region-by region-id 1 1)))
|
|
(ebox-layer-scroll-test--assert-fresh)))))
|
|
|
|
(ert-deftest ebox-layer-scroll-visible-root-dependencies-retain-composed-cache ()
|
|
"Pure root scroll slices already composed visible projections and anchors."
|
|
(dolist (target '(portal anchor))
|
|
(ebox-layer-scroll-test--with-buffer
|
|
(ebox-render-to-buffer
|
|
(current-buffer) (ebox-layer-scroll-test--input t t))
|
|
(if (eq target 'portal)
|
|
(ebox-region-update "portal" :visibility 'visible)
|
|
(ebox-region-update "upper" :anchor "before"))
|
|
(let* ((state (ebox-layer-scroll-test--root-state))
|
|
(root (plist-get state :box))
|
|
(region-id (plist-get root :region-id))
|
|
(render (symbol-function 'ebox-surface--render-candidate))
|
|
(root-renders 0))
|
|
(should (ebox-layer-cached-root-scroll-p root state))
|
|
(cl-letf (((symbol-function 'ebox-surface--render-candidate)
|
|
(lambda (&rest arguments)
|
|
(cl-incf root-renders)
|
|
(apply render arguments))))
|
|
(should (= 1 (ebox--scroll-region-by region-id 1 1))))
|
|
(should (= root-renders 0))
|
|
(ebox-layer-scroll-test--assert-fresh)))))
|
|
|
|
(ert-deftest ebox-layer-scroll-anchor-provider-chain-crosses-viewport ()
|
|
"Cached provider chains keep text and interactions when entering and leaving."
|
|
(ebox-layer-scroll-test--with-buffer
|
|
(ebox-render-to-buffer (current-buffer) (ebox-layer-scroll-test--portal-chain-input))
|
|
(let* ((state (ebox-layer-scroll-test--root-state))
|
|
(region-id (plist-get (plist-get state :box) :region-id))
|
|
(content (plist-get state :content-lines))
|
|
(visible 0) (outside 0))
|
|
(should (string-match-p "CHAIN" (ebox-lines-join content)))
|
|
(dolist (delta '(1 1 1 1 1 1 -1 -1 -1 -1 -1 -1))
|
|
(cl-letf (((symbol-function 'ebox-surface--render-candidate)
|
|
(lambda (&rest _) (ert-fail "Root scroll recomposed portals"))))
|
|
(should (= delta (ebox--scroll-region-by region-id delta 1))))
|
|
(should (eq content (plist-get (ebox-layer-scroll-test--root-state)
|
|
:content-lines)))
|
|
(if (string-match-p "CHAIN" (buffer-string))
|
|
(cl-incf visible)
|
|
(cl-incf outside))
|
|
(ebox-layer-scroll-test--assert-fresh))
|
|
(should (> visible 0))
|
|
(should (> outside 0)))))
|
|
|
|
(ert-deftest ebox-layer-scroll-anchor-provider-invalidation-and-rollback ()
|
|
"Provider changes replace composed caches; rejected scrolls preserve them."
|
|
(ebox-layer-scroll-test--with-buffer
|
|
(ebox-render-to-buffer (current-buffer) (ebox-layer-scroll-test--portal-chain-input))
|
|
(let* ((state (ebox-layer-scroll-test--root-state))
|
|
(region-id (plist-get (plist-get state :box) :region-id)))
|
|
(should (= 3 (ebox--scroll-region-by region-id 3 3)))
|
|
(dolist (change '(("provider-anchor" :content "EDIT")
|
|
("provider" :placement top-end)
|
|
("trigger" :left (ch 3))
|
|
("clipped-owner" :visibility hidden)
|
|
("clipped-owner" :visibility visible)))
|
|
(let ((content (plist-get (ebox-layer-scroll-test--root-state) :content-lines)))
|
|
(apply #'ebox-region-update change)
|
|
(should-not (eq content (plist-get (ebox-layer-scroll-test--root-state)
|
|
:content-lines))))
|
|
(ebox-layer-scroll-test--assert-fresh)
|
|
(should (= 1 (ebox--scroll-region-by region-id 1 1)))
|
|
(ebox-layer-scroll-test--assert-fresh)
|
|
(should (= -1 (ebox--scroll-region-by region-id -1 1))))
|
|
(dolist (failure-step '(text client-state))
|
|
(let ((runtime (ebox--buffer-render-state (current-buffer)))
|
|
(content (plist-get (ebox-layer-scroll-test--root-state) :content-lines))
|
|
(before (buffer-string))
|
|
(revision (ebox-surface-buffer-revision (current-buffer))))
|
|
(let ((tp--surface-publication-step-function
|
|
(lambda (step _surface)
|
|
(when (eq step failure-step) (error "Reject anchored root scroll")))))
|
|
(should-error (ebox--scroll-region-by region-id 1 1)))
|
|
(should (eq runtime (ebox--buffer-render-state (current-buffer))))
|
|
(should (eq content (plist-get (ebox-layer-scroll-test--root-state) :content-lines)))
|
|
(should (= revision (ebox-surface-buffer-revision (current-buffer))))
|
|
(should (equal-including-properties before (buffer-string)))))
|
|
(should (= 1 (ebox--scroll-region-by region-id 1 1)))
|
|
(ebox-layer-scroll-test--assert-fresh))))
|
|
|
|
(ert-deftest ebox-layer-scroll-declines-incomplete-or-changed-cache ()
|
|
"Dirty or lazy content and simultaneous source changes cannot reuse layers."
|
|
(ebox-layer-scroll-test--with-buffer
|
|
(ebox-render-to-buffer
|
|
(current-buffer) (ebox-layer-scroll-test--input t))
|
|
(let* ((runtime (ebox--buffer-render-state (current-buffer)))
|
|
(state (ebox-layer-scroll-test--root-state))
|
|
(root (plist-get state :box))
|
|
(owner (plist-get root :node-id)))
|
|
(dolist (key '(:render-content-prefix :materialize-content-lines
|
|
:lazy-scroll-prefix-dirty :lazy-scroll-window-refresh-required))
|
|
(let ((candidate (copy-sequence state)))
|
|
(plist-put candidate :content-lines-complete-p nil)
|
|
(setq candidate (plist-put candidate key t))
|
|
(should-not (ebox-layer-cached-root-scroll-p root candidate))))
|
|
(dolist (keys '((:scroll-offset :content) (:visibility) (:width)))
|
|
(should-not
|
|
(ebox-incremental--layer-root-scroll-p
|
|
runtime runtime
|
|
(list :dirty-set (list (list :node-id owner :changed-keys keys)))
|
|
owner))))))
|
|
|
|
(ert-deftest ebox-layer-scroll-ordinary-materialized-content-builds-rendered-rows ()
|
|
"Ordinary materialization still bootstraps its reusable physical row cache."
|
|
(ebox-layer-scroll-test--with-buffer
|
|
(ebox-render-to-buffer
|
|
(current-buffer)
|
|
(ebox-build
|
|
`(column :id "root" :width (vw 100) :height (vh 100) :overflow scroll
|
|
,@(cl-loop for index below 80
|
|
collect `(box :padding-left (px 1.5)
|
|
,(format "ROW%02d" index))))))
|
|
(let* ((runtime (ebox--buffer-render-state (current-buffer)))
|
|
(state (ebox-layer-scroll-test--root-state))
|
|
(region-id (plist-get (plist-get state :box) :region-id)))
|
|
(should-not (plist-get runtime :layered-p))
|
|
(should (plist-get runtime :fractional-pixel-output-p))
|
|
(setq state (ebox--scroll-state-materialize-lines region-id state))
|
|
(puthash region-id state (plist-get runtime :scroll-state-table))
|
|
(should (plist-get state :content-lines-complete-p))
|
|
(should-not (plist-get state :rendered-content-lines))
|
|
(should (= 1 (ebox--scroll-region-by region-id 1 1)))
|
|
(should (= -1 (ebox--scroll-region-by region-id -1 1)))
|
|
(should (plist-get (ebox-layer-scroll-test--root-state) :rendered-content-lines))
|
|
(cl-letf (((symbol-function 'ebox-surface--render-candidate-node)
|
|
(lambda (&rest _) (ert-fail "Cached ordinary scroll rendered wrapper"))))
|
|
(should (= 1 (ebox--scroll-region-by region-id 1 1))))
|
|
(ebox-layer-scroll-test--assert-fresh))))
|
|
|
|
(provide 'ebox-layer-scroll-tests)
|
|
;;; ebox-layer-scroll-tests.el ends here
|