From 3fadb1912184308c8ea75793536484b266ac205f Mon Sep 17 00:00:00 2001 From: Kinneyzhang Date: Sun, 6 Sep 2026 14:00:59 +0800 Subject: [PATCH] refactor: preserve line data through box decoration --- Makefile | 7 +- ebox-layout.el | 31 ++- ebox.el | 56 +++-- tests/ebox-box-line-flow-tests.el | 355 ++++++++++++++++++++++++++++++ tests/ebox-core-render-tests.el | 6 +- 5 files changed, 419 insertions(+), 36 deletions(-) create mode 100644 tests/ebox-box-line-flow-tests.el diff --git a/Makefile b/Makefile index fcea602..6264a1a 100644 --- a/Makefile +++ b/Makefile @@ -10,7 +10,7 @@ NATIVE_MANIFEST = native/Cargo.toml NATIVE_TARGET ?= $(shell $(EMACS_BATCH) -l ebox-native-reflow.el --eval '(princ (ebox-native-reflow--rust-target))') NATIVE_RELEASE_DIR = native/target/$(NATIVE_TARGET)/release -.PHONY: all check ci load compile test checkdoc source-tests font-tests core-tests child-range-tests grid-tests ebox-commit-tests surface-tests visual-check-tests package-tests selector-tests dsl-tests flex-tests viewport-topology-tests vertical-materialization-tests m0a-tests state-contract-tests layout-boundary-tests layout-boundary-performance patch-plan-tests patch-plan-performance style-schema-tests style-schema-performance spi-tests spi-performance c1b-contract-tests docs-contract-tests ci-contract-tests performance-evaluator visual-check native-rust-tests native-build diff-check clean package-lint package-lint-install +.PHONY: all check ci load compile test checkdoc source-tests font-tests core-tests child-range-tests grid-tests ebox-commit-tests surface-tests visual-check-tests package-tests selector-tests dsl-tests flex-tests viewport-topology-tests vertical-materialization-tests box-line-flow-tests m0a-tests state-contract-tests layout-boundary-tests layout-boundary-performance patch-plan-tests patch-plan-performance style-schema-tests style-schema-performance spi-tests spi-performance c1b-contract-tests docs-contract-tests ci-contract-tests performance-evaluator visual-check native-rust-tests native-build diff-check clean package-lint package-lint-install all: check @@ -25,7 +25,7 @@ compile: rm -f *.elc tests/*.elc scripts/*.elc $(EMACS_BATCH) --eval '(setq byte-compile-error-on-warn t byte-compile-warnings (quote (not obsolete)))' -l ebox.el --eval '(ebox-byte-compile)' -test: source-tests font-tests core-tests child-range-tests grid-tests ebox-commit-tests surface-tests visual-check-tests package-tests selector-tests dsl-tests flex-tests viewport-topology-tests vertical-materialization-tests m0a-tests state-contract-tests layout-boundary-tests patch-plan-tests style-schema-tests spi-tests docs-contract-tests ci-contract-tests +test: source-tests font-tests core-tests child-range-tests grid-tests ebox-commit-tests surface-tests visual-check-tests package-tests selector-tests dsl-tests flex-tests viewport-topology-tests vertical-materialization-tests box-line-flow-tests m0a-tests state-contract-tests layout-boundary-tests patch-plan-tests style-schema-tests spi-tests docs-contract-tests ci-contract-tests source-tests: $(EMACS_BATCH) -l tests/ebox-source-tests.el -f ert-run-tests-batch-and-exit @@ -69,6 +69,9 @@ viewport-topology-tests: vertical-materialization-tests: $(EMACS_TEST) -l tests/ebox-vertical-materialization-tests.el -f ert-run-tests-batch-and-exit +box-line-flow-tests: + $(EMACS_TEST) -l tests/ebox-box-line-flow-tests.el -f ert-run-tests-batch-and-exit + m0a-tests: $(EMACS_TEST) -l tests/ebox-m0a-inventory-fixture.el -l tests/ebox-m0a-inventory-tests.el -l tests/ebox-m0a-characterization-tests.el -f ert-run-tests-batch-and-exit diff --git a/ebox-layout.el b/ebox-layout.el index 33ee3e5..949487b 100644 --- a/ebox-layout.el +++ b/ebox-layout.el @@ -39,6 +39,10 @@ (declare-function ebox--line-content-metadata-uniform-p "ebox" (line)) (declare-function ebox--lines-align-vertical "ebox" (string height align)) +(declare-function ebox--pad-lines-vertical + "ebox" (lines height &optional offset padding-string)) +(declare-function ebox--vertical-alignment-offset + "ebox" (line-count height align)) (declare-function ebox--lines-concat-horizontal "ebox" (&rest strings)) (declare-function ebox--lines-stack-vertical "ebox" (&rest strings)) (declare-function ebox--add-content-owner @@ -1669,12 +1673,11 @@ Internal implementation of `ebox-render' for box nodes." (t (let ((ebox--propertize-private-content-line-p private-formatted-lines-p)) - (ebox-lines-join - (cl-loop for line in visible-lines - for idx from 0 - collect (ebox--propertize-content-line - line region-id idx - padding-line-filler)))))))) + (cl-loop for line in visible-lines + for idx from 0 + collect (ebox--propertize-content-line + line region-id idx + padding-line-filler))))))) ;; Rendering is the first point where the complete formatted height is ;; known. Keep the host box and published scroll state on the same @@ -1691,9 +1694,15 @@ Internal implementation of `ebox-render' for box nodes." ;; 1. Vertical Alignment of content within the box height. Reused scroll ;; lines already fill a top-aligned, chrome-free viewport. (unless simple-scroll-rendered-lines - (setq result (ebox--lines-align-vertical - result content-height - (ebox-get box :vertical-align)))) + ;; The former join treated no decorated lines as one empty string line. + ;; Keep that string-adapter convention outside the list pad/clip core. + (let ((lines (or result (list "")))) + (setq result + (ebox--pad-lines-vertical + lines content-height + (ebox--vertical-alignment-offset + (length lines) content-height + (ebox-get box :vertical-align)))))) ;; Fill any empty lines generated by alignment with correct width spaces ;; 保留原有的 ebox-content/ebox-content-idx 属性 @@ -1717,7 +1726,9 @@ Internal implementation of `ebox-render' for box nodes." 'ebox-content-idx 0 'ebox-content-owner region-id))) line)) - (ebox-string-lines result)))) + ;; Splitting the former empty aligned string yielded one line, + ;; even when alignment clipped all content at height zero. + (or result (list ""))))) ;; Scroll state describes formatted content, independently of chrome. (puthash region-id box ebox--region-box-table) diff --git a/ebox.el b/ebox.el index 577213b..9df7328 100644 --- a/ebox.el +++ b/ebox.el @@ -589,6 +589,36 @@ segments that incremental publication still needs." '(ebox-content ebox-content-idx ebox-content-owner ebox-content-owners))))) +(defun ebox--pad-lines-vertical (lines height &optional offset padding-string) + "Pad or clip LINES to HEIGHT without changing their strings or list spine. +OFFSET shifts content toward the bottom when positive and toward the top when +negative. PADDING-STRING defaults to a blank matching the first line's width. +The result may borrow LINES or their strings. Treat it as read-only. +Empty input has zero lines before padding. Height zero clips to nil." + (let ((count (length lines))) + (if (> height count) + (let* ((fill (or padding-string + (ebox--pixel-blank + (ebox--string-pixel-width (car lines)) 1) + "")) + (rest (- height count)) + (offset (or offset 0)) + (top (if (>= offset 0) (min offset rest) + (max 0 (+ offset rest)))) + (bottom (- rest top))) + (append (make-list top fill) + (if (> bottom 0) + (append lines (make-list bottom fill)) + lines))) + (if (= height count) lines (seq-take lines height))))) + +(defun ebox--vertical-alignment-offset (line-count height align) + "Return the offset for LINE-COUNT lines in HEIGHT using ALIGN." + (pcase align + ('bottom (- height line-count)) + ('center (/ (- height line-count) 2)) + (_ 0))) + (defun ebox--lines-pad-vertical (string height &optional offset padding-string) "Pad or clip STRING to HEIGHT lines. OFFSET controls content shift: positive (top), negative (bottom). @@ -596,31 +626,15 @@ PADDING-STRING fills added lines; its default matches STRING's first-line width. (if (and (stringp string) (integerp height) (= height 1) (not (string-match-p "\n" string))) (copy-sequence string) - (let* ((lines (ebox-string-lines string)) - (line-count (length lines))) - (when (> height line-count) - (let* ((padding-string (or padding-string - (ebox--pixel-blank - (ebox--string-pixel-width string) 1))) - (rest (- height line-count)) - (offset (or offset 0)) - (top-pad (cond ((>= offset 0) (min offset rest)) - (t (max 0 (+ offset rest))))) - (bottom-pad (- rest top-pad)) - (pad-lines (make-list top-pad padding-string)) - (bottom-lines (make-list bottom-pad padding-string))) - (setq lines (append pad-lines lines bottom-lines)))) - (ebox-lines-join (seq-take lines height))))) + (ebox-lines-join + (ebox--pad-lines-vertical + (ebox-string-lines string) height offset padding-string)))) (defun ebox--lines-align-vertical (string height align) "Align STRING to HEIGHT lines. ALIGN can be `top', `center', or `bottom'." - (let* ((line-count (ebox-string-height string)) - (offset (pcase align - ('top 0) - ('bottom (- height line-count)) - ('center (/ (- height line-count) 2)) - (_ 0)))) + (let ((offset (ebox--vertical-alignment-offset + (ebox-string-height string) height align))) (ebox--lines-pad-vertical string height offset))) (defun ebox--string-lines-pad-bottom (string height) diff --git a/tests/ebox-box-line-flow-tests.el b/tests/ebox-box-line-flow-tests.el new file mode 100644 index 0000000..e641e6c --- /dev/null +++ b/tests/ebox-box-line-flow-tests.el @@ -0,0 +1,355 @@ +;;; ebox-box-line-flow-tests.el --- Box line flow contracts -*- lexical-binding: t; -*- + +(require 'cl-lib) +(require 'ert) +(setq load-prefer-newer t) +(load-file (expand-file-name "../ebox.el" + (file-name-directory load-file-name))) + +(defun ebox-box-line-test--previous-pad (string height &optional offset padding) + "Apply the frozen string padding algorithm to STRING at HEIGHT." + (let* ((lines (ebox-string-lines string)) + (count (length lines))) + (when (> height count) + (let* ((fill (or padding + (ebox--pixel-blank (ebox--string-pixel-width string) 1))) + (rest (- height count)) + (shift (or offset 0)) + (top (if (>= shift 0) (min shift rest) + (max 0 (+ shift rest))))) + (setq lines (append (make-list top fill) lines + (make-list (- rest top) fill))))) + (ebox-lines-join (seq-take lines height)))) + +(defun ebox-box-line-test--previous-prepare (box formatted height region-id filler) + "Freeze BOX's old join/align/split/blank-normalization front half. +FORMATTED is captured before decoration; HEIGHT and REGION-ID are the actual +render constraints. This oracle does not call the new line operation." + (let* ((text-height (ebox-string-height formatted)) + (scroll-p (eq (ebox-get box :overflow) 'scroll)) + (start (if scroll-p + (max 0 (min (or (ebox-get box :scroll-offset) 0) + (max 0 (- text-height height)))) + 0)) + (lines (ebox-string-lines formatted)) + (visible (if scroll-p + (seq-subseq lines start (min (+ start height) text-height)) + (seq-take lines height))) + (ebox--propertize-private-content-line-p + (not (and scroll-p (> text-height height)))) + (decorated + (ebox-lines-join + (cl-loop for line in visible for index from 0 + collect (ebox--propertize-content-line + line region-id index filler)))) + (count (ebox-string-height decorated)) + (offset (pcase (ebox-get box :vertical-align) + ('bottom (- height count)) + ('center (/ (- height count) 2)) + (_ 0))) + (aligned (ebox-box-line-test--previous-pad decorated height offset))) + (mapcar + (lambda (line) + (if (and (string-empty-p (string-trim line)) + (ebox--line-content-metadata-uniform-p line) + (not (ebox--line-has-non-content-properties-p line))) + (let ((id (get-text-property 0 'ebox-content line)) + (index (get-text-property 0 'ebox-content-idx line))) + (propertize filler 'ebox-content (or id region-id) + 'ebox-content-idx (if id index 0) + 'ebox-content-owner region-id)) + line)) + (ebox-string-lines aligned)))) + +(defun ebox-box-line-test--reference-render (input &optional actual-p) + "Render INPUT with the old front half, or current code when ACTUAL-P. +Check opaque identity between the actual formatted source and compositor output; +the surrounding ephemeral TP surface has its own snapshot value ownership." + (let ((format-function (symbol-function 'ebox--format-content)) + (compositor (symbol-function 'ebox--window-render-flat-wrapper-chunk-lines)) + (formatted (make-hash-table :test 'eq))) + (cl-letf (((symbol-function 'ebox--format-content) + (lambda (box) + (let ((result (funcall format-function box))) + (puthash box + (list (copy-sequence result) + (plist-get ebox--scroll-window-render-result + :content-height)) + formatted) + result))) + ((symbol-function 'ebox--window-render-flat-wrapper-chunk-lines) + (lambda (&rest args) + (when (and (nth 7 args) (not actual-p)) + (let* ((box (car args)) + (record (gethash box formatted)) + (text (car record)) + (height (or (cadr record) + (ebox--content-height + box (ebox-string-height text))))) + (should record) + (setf (nth 1 args) + (ebox-box-line-test--previous-prepare + box text height (nth 3 args) (nth 4 args))))) + (let* ((result (apply compositor args)) + (source (car (gethash (car args) formatted))) + (source-pos (and source (string-match "Open" source))) + (line (cl-find-if (lambda (value) (string-match "Open" value)) + result))) + (when (and source-pos line) + (let ((pos (string-match "Open" line))) + (dolist (property '(keymap help-echo)) + (should (eq (get-text-property pos property line) + (get-text-property source-pos property source)))))) + result)))) + (ebox-render input)))) + +(defun ebox-box-line-test--materializations (input) + "Record actual intermediate newline joins/splits while rendering INPUT's root." + (let* ((handle (ebox-node-source-handle (car (ebox-canonical-input-roots input)))) + (render (symbol-function 'ebox--render-box)) + (format-function (symbol-function 'ebox--format-content)) + (join (symbol-function 'mapconcat)) + (slice (symbol-function 'substring)) + (joined (make-hash-table :test 'eq)) + (joins 0) (splits 0) active formatting output) + (cl-letf (((symbol-function 'ebox--render-box) + (lambda (box) + (let ((previous active)) + (setq active (eq (ebox-node-source-handle box) handle)) + (unwind-protect (funcall render box) + (setq active previous))))) + ((symbol-function 'ebox--format-content) + (lambda (box) + (let ((previous formatting)) + (setq formatting t) + (unwind-protect (funcall format-function box) + (setq formatting previous))))) + ((symbol-function 'mapconcat) + (lambda (function sequence separator) + (let ((result (funcall join function sequence separator))) + (when (and active (not formatting) (equal separator "\n") + (string-match-p "alpha" result)) + (cl-incf joins) + (puthash result t joined)) + result))) + ((symbol-function 'substring) + (lambda (string from &optional to) + (when (and active (not formatting) (gethash string joined)) + (cl-incf splits)) + (funcall slice string from to)))) + (setq output (ebox-render input))) + (list :output output :joins joins :splits splits))) + +(ert-deftest ebox-box-line-flow-avoids-intermediate-join-and-split () + "Ordinary decoration should join only at the existing compositor exit." + (let* ((input (ebox-build + '(box :width (120) :height 8 :overflow hidden + :padding (1 (3)) :border (1 solid "#112233") + :bgcolor "#ddeeff" (text "alpha\nbeta")))) + (expected (ebox-box-line-test--reference-render input)) + (observed (ebox-box-line-test--materializations input))) + (should (equal-including-properties (plist-get observed :output) expected)) + (ert-info ((format "Actual content joins=%S, joined-string substrings=%S" + (plist-get observed :joins) (plist-get observed :splits))) + (should (= (plist-get observed :joins) 1)) + (should (= (plist-get observed :splits) 0))))) + +(ert-deftest ebox-box-line-flow-preserves-old-front-half-output () + "Composition, chrome, visibility and ownership match the frozen string path." + (let* ((map (make-sparse-keymap)) + (callback (let ((value 'opened)) (lambda () value))) + (rich (concat (propertize "Open 中" 'keymap map 'help-echo callback) + (propertize "\n" 'help-echo "newline only") + (propertize " " 'help-echo "rich blank") "\ntail\n"))) + (define-key map [mouse-1] #'ignore) + (dolist (kind '(box row column flex grid)) + (dolist (height '(0 4 8)) + (dolist (align '(top center bottom)) + (dolist (overflow '(hidden visible scroll)) + (let* ((child (pcase kind + ('box `(box (text ,rich))) + ('grid `(grid :grid-template-columns ((110)) (text ,rich))) + (_ `(,kind (text ,rich))))) + (input + (ebox-build + `(box :width (140) :height ,height :overflow ,overflow + :padding (1 (3)) + :border (1 solid "#112233") :margin (1 (2)) + :color "#123456" :bgcolor "#ddeeff" ,child))) + (_alignment + (plist-put (car (ebox-canonical-input-roots input)) + :vertical-align align)) + (source-before (copy-sequence rich)) + (actual (ebox-box-line-test--reference-render input t)) + (expected (ebox-box-line-test--reference-render input))) + (ert-info ((format "%S height=%S align=%S overflow=%S" + kind height align overflow)) + (should (equal-including-properties actual expected)) + (should (equal-including-properties rich source-before)) + (when-let* ((pos (string-match "Open" actual))) + (should (eq (lookup-key (get-text-property pos 'keymap actual) + [mouse-1]) #'ignore)) + (should (eq (funcall (get-text-property pos 'help-echo actual)) + 'opened)) + (should (> (length (get-text-property + pos 'ebox-content-owners actual)) 1))))))))))) + +(ert-deftest ebox-box-line-flow-keeps-string-padding-contract () + "String padding retains nil, empty, first-line width and multiline filler." + (dolist (input (list nil "" "A" "A\nlonger" "\n" "A\n")) + (dolist (height '(0 1 2 4)) + (dolist (offset '(nil -2 0 2)) + (dolist (filler (list nil "" (propertize "L\nR" 'help-echo "fill"))) + (should (equal-including-properties + (ebox--lines-pad-vertical input height offset filler) + (ebox-box-line-test--previous-pad input height offset filler))))))) + (let* ((input "A\nlonger") + (output (ebox-string-lines (ebox--lines-pad-vertical input 3))) + (fill (car (last output)))) + (should (= (ebox--string-pixel-width fill) (ebox--string-pixel-width "A"))))) + +(ert-deftest ebox-box-line-flow-keeps-simple-scroll-output () + "Complete simple scroll keeps its single join for overflow and underfill." + (dolist (text '("alpha\nbeta" "alpha\n2\n3\n4\n5\n6\n7\n8\n9")) + (let* ((input (ebox-build + `(box :width (120) :height 8 :overflow scroll + :bgcolor "#ddeeff" (text ,text)))) + (expected (ebox-box-line-test--reference-render input)) + (observed (ebox-box-line-test--materializations input))) + (should (equal-including-properties (plist-get observed :output) expected)) + (should (= (plist-get observed :joins) 1)) + (should (= (plist-get observed :splits) 0))))) + +(ert-deftest ebox-box-line-flow-list-core-borrows-without-mutating () + "Line padding preserves the original spine, strings and opaque property values." + (let* ((map (make-sparse-keymap)) + (callback (let ((value 'opened)) (lambda () value))) + (rich (propertize "Open 中" 'keymap map 'help-echo callback + 'ebox-content 17 'ebox-content-idx 2 + 'ebox-content-owner 18 'ebox-content-owners '(18 17))) + (blank (propertize " " 'display '(space :width (7)) + 'ebox-content-owner 19))) + (dolist (lines (list nil (list "") (list rich) (list rich "" blank))) + (dolist (height '(0 1 3 5)) + (dolist (align '(top center bottom)) + (let* ((spine (cl-loop for tail on lines + collect (list tail (car tail) (cdr tail) + (copy-sequence (car tail))))) + (count (length lines)) + (offset (pcase align + ('bottom (- height count)) + ('center (/ (- height count) 2)) + (_ 0))) + (expected (ebox-box-line-test--previous-pad + (and lines (ebox-lines-join lines)) height offset)) + (actual (ebox--pad-lines-vertical lines height offset)) + (output (ebox-lines-join actual))) + (should (equal-including-properties output expected)) + (when (= height count) (should (eq actual lines))) + (when-let* ((pos (string-match "Open" output))) + (should (eq (get-text-property pos 'keymap output) map)) + (should (eq (get-text-property pos 'help-echo output) callback))) + ;; Only the string boundary grants independent mutable ownership. + (when (> (length output) 0) + (put-text-property 0 1 'help-echo "output-only" output) + (aset output 0 ?Z)) + (dolist (record spine) + (should (eq (car (nth 0 record)) (nth 1 record))) + (should (eq (cdr (nth 0 record)) (nth 2 record))) + (should (equal-including-properties (nth 1 record) (nth 3 record))))))))) + ;; The list primitive does not own either legacy string-adapter bridge. + (should-not (ebox--pad-lines-vertical nil 0)) + (should-not (ebox--pad-lines-vertical (list "A") 0))) + +(ert-deftest ebox-box-line-flow-retains-empty-string-adapter-bridges () + "Empty formatted content and zero used height retain the former blank line." + (dolist (formatted '(nil "" "A\nB")) + (dolist (height '(0 1 3)) + (let* ((input (ebox-build + '(box :width (40) :height 3 :overflow hidden :padding (1 (2)) + :border (1 solid "#112233") (text "unused")))) + (root (car (ebox-canonical-input-roots input))) + (handle (ebox-node-source-handle root)) + (format-function (symbol-function 'ebox--format-content)) + (height-function (symbol-function 'ebox--content-height))) + ;; Isolate the decoration boundary, including a zero used content + ;; height; the ordinary public height resolver clamps to at least one. + (cl-letf (((symbol-function 'ebox--format-content) + (lambda (box) + (if (eq (ebox-node-source-handle box) handle) + formatted + (funcall format-function box)))) + ((symbol-function 'ebox--content-height) + (lambda (box natural) + (if (eq (ebox-node-source-handle box) handle) + height + (funcall height-function box natural))))) + (let ((actual (ebox-box-line-test--reference-render input t)) + (expected (ebox-box-line-test--reference-render input))) + (should (equal-including-properties actual expected)) + (when (= height 0) + (should (= (ebox-string-height actual) 3))))))))) + +(ert-deftest ebox-box-line-flow-zero-width-default-padding-is-a-line () + "Zero-width default blanks remain empty string lines at the list boundary." + (dolist (padding '(0 (1 (2)))) + (let* ((input (ebox-build + `(box :width (0) :height 3 :overflow hidden + :padding ,padding (text "")))) + (actual (ebox-box-line-test--reference-render input t)) + (expected (ebox-box-line-test--reference-render input))) + (should (equal-including-properties actual expected))))) + +(ert-deftest ebox-box-line-flow-scroll-to-end-and-back-preserves-source () + "Real decorated scroll A→B→A keeps stored lines intact and reaches the end." + (let* ((buffer (generate-new-buffer " *ebox-box-line-scroll*")) + (map (make-sparse-keymap)) + (_key (define-key map [mouse-1] #'ignore)) + (text (mapconcat (lambda (index) (format "Open %02d" index)) + (number-sequence 0 9) "\n")) + (input (ebox-build + `(box :width (120) :height 4 :overflow scroll + :padding (0 (2)) :border (1 solid "#112233") + (text ,(propertize text 'keymap map 'help-echo "action"))))) + (ebox-runtime-idle-prewarm nil) + (ebox-runtime-idle-reflow-cache-prewarm nil)) + (unwind-protect + (progn + (ebox-render-to-buffer buffer input) + (with-current-buffer buffer + (let* ((root (plist-get (ebox--buffer-render-state buffer) :root-node)) + (id (plist-get root :region-id)) + (state (ebox--scroll-get-state id)) + (lines (plist-get state :content-lines)) + (spine (cl-loop for tail on lines + collect (list tail (car tail) (cdr tail) + (copy-sequence (car tail))))) + (initial (buffer-string)) + (end (max 0 (- (length lines) (plist-get state :content-height))))) + (should state) + (goto-char (point-min)) + (ebox-scroll-down end) + (let* ((current (ebox--scroll-get-state id)) + (render-state (ebox--buffer-render-state buffer)) + (fresh-input + (ebox-canonical-input-create + (list (plist-get render-state :root-node)) + (plist-get render-state :source-index)))) + (should (= end (max 0 (- (length (plist-get current :content-lines)) + (plist-get current :content-height))))) + (should (= (plist-get current :scroll-offset) end)) + (should (> end 0)) + (should (string-match-p "Open 09" (buffer-string))) + (should (equal-including-properties + (buffer-string) (ebox-box-line-test--reference-render fresh-input)))) + (ebox-scroll-up end) + (should (= (plist-get (ebox--scroll-get-state id) :scroll-offset) 0)) + (should (equal-including-properties (buffer-string) initial)) + (dolist (record spine) + (should (eq (car (nth 0 record)) (nth 1 record))) + (should (eq (cdr (nth 0 record)) (nth 2 record))) + (should (equal-including-properties (nth 1 record) (nth 3 record))))))) + (when (buffer-live-p buffer) (kill-buffer buffer))))) + +(provide 'ebox-box-line-flow-tests) +;;; ebox-box-line-flow-tests.el ends here diff --git a/tests/ebox-core-render-tests.el b/tests/ebox-core-render-tests.el index 54cdb0a..dd88643 100644 --- a/tests/ebox-core-render-tests.el +++ b/tests/ebox-core-render-tests.el @@ -4657,11 +4657,11 @@ line compositor's complete property output, including empty lines and chrome." (observed (ebox-test--box-full-height-joins input 6)) (joins (plist-get observed :joins))) (should (= (ebox-string-height (plist-get observed :rendered)) 8)) - ;; Alignment joins once; blank normalization retains its line result and - ;; the shared compositor adds only the final border-box join. + ;; Alignment and blank normalization retain lines; the shared compositor + ;; performs the only full-height join at the border-box output boundary. (ert-info ((format "Materialized (height bytes): %S" joins)) - (should (= (length joins) 2))))) + (should (= (length joins) 1))))) (ert-deftest ebox-box-chrome-keeps-complete-scroll-materialization () "Existing fully painted scroll output must not enter decoration again."