4158 lines
180 KiB
EmacsLisp
4158 lines
180 KiB
EmacsLisp
;;; tp-tests.el --- ERT tests for tp.el -*- lexical-binding: t -*-
|
|
|
|
;; Copyright (C) 2024-2026
|
|
|
|
;;; Commentary:
|
|
|
|
;; Comprehensive test suite for tp.el using ERT (Emacs Lisp Regression Testing).
|
|
;; Run with:
|
|
;; emacs --batch -L . -L tests -l tp.el -l tp-tests.el \
|
|
;; -f ert-run-tests-batch-and-exit
|
|
|
|
;;; Code:
|
|
|
|
(require 'ert)
|
|
(require 'cl-lib)
|
|
(require 'tp-palette)
|
|
(require 'tp)
|
|
|
|
;;; ============================================================
|
|
;;; Test Utilities
|
|
;;; ============================================================
|
|
|
|
(defmacro tp-test-with-temp-buffer (&rest body)
|
|
"Execute BODY in a temporary buffer with a clean tp state.
|
|
All layer registries, transforms and reactive watchers are cleared
|
|
before BODY runs and again afterwards (teardown), so state cannot
|
|
leak between tests regardless of how BODY exits."
|
|
(declare (indent 0))
|
|
`(unwind-protect
|
|
(with-temp-buffer
|
|
(tp-layer-reset)
|
|
,@body)
|
|
(tp-layer-reset)))
|
|
|
|
;; Reactive test variables set with `setq' inside tests. They must be
|
|
;; dynamically bound (variable watchers depend on it), so plain
|
|
;; `defvar' declarations are used.
|
|
(defvar tp-test-first-name nil "Test variable for computed properties.")
|
|
(defvar tp-test-last-name nil "Test variable for computed properties.")
|
|
(defvar tp-test-full-name nil "Test variable for computed properties.")
|
|
(defvar tp-test-dc-color nil "Test variable for data+compute layer.")
|
|
(defvar tp-test-dc-first nil "Test variable for data+compute layer.")
|
|
(defvar tp-test-dc-last nil "Test variable for data+compute layer.")
|
|
(defvar tp-test-dc-full-name nil "Test variable for data+compute layer.")
|
|
(defvar tp-test-init-color nil "Test variable for initial values.")
|
|
(defvar tp-test-init-name nil "Test variable for initial values.")
|
|
(defvar tp-test-init-other nil "Test variable for initial values.")
|
|
(defvar tp-test-global-color nil "Test variable for global updates.")
|
|
(defvar tp-test-redef-color nil "Test variable for layer re-definition.")
|
|
(defvar tp-test-watch-var nil "Test variable for watch callbacks.")
|
|
(defvar tp-test-compute-src nil "Test variable for compute source.")
|
|
(defvar tp-test-compute-out nil "Test variable for compute output.")
|
|
(defvar tp-test-group-color nil "Test variable for layer groups.")
|
|
(defvar tp-test-name-part1 nil "Test variable for tp-text updates.")
|
|
(defvar tp-test-name-part2 nil "Test variable for tp-text updates.")
|
|
(defvar tp-test-batch-color nil "Test variable for batch updates.")
|
|
(defvar tp-test-fg nil "Test variable for batch foreground.")
|
|
(defvar tp-test-bg nil "Test variable for batch background.")
|
|
(defvar tp-test-amount nil "Test variable for transform updates.")
|
|
|
|
;;; ============================================================
|
|
;;; Basic Text Property Functions Tests
|
|
;;; ============================================================
|
|
|
|
(ert-deftest tp-test-put-and-get ()
|
|
"Test tp-set and tp-get basic functionality."
|
|
(tp-test-with-temp-buffer
|
|
(insert "Hello World")
|
|
;; Set a single property
|
|
(tp-set 1 6 '(face bold))
|
|
(should (eq (tp-at 1 'face) 'bold))
|
|
(should (eq (tp-at 3 'face) 'bold))
|
|
(should (null (tp-at 7 'face)))
|
|
;; Set multiple properties
|
|
(tp-set 7 12 '(face italic help-echo "test"))
|
|
(should (eq (tp-at 7 'face) 'italic))
|
|
(should (equal (tp-at 7 'help-echo) "test"))))
|
|
|
|
(ert-deftest tp-test-put-with-list ()
|
|
"Test tp-set accepts properties as a list."
|
|
(tp-test-with-temp-buffer
|
|
(insert "Hello")
|
|
(tp-set 1 6 '(face bold help-echo "greeting"))
|
|
(should (eq (tp-at 1 'face) 'bold))
|
|
(should (equal (tp-at 1 'help-echo) "greeting"))))
|
|
|
|
(ert-deftest tp-test-put-returns-region ()
|
|
"Test tp-set returns the modified region."
|
|
(tp-test-with-temp-buffer
|
|
(insert "Hello")
|
|
(let ((result (tp-set 1 6 '(face bold))))
|
|
(should (equal result '(1 . 6))))))
|
|
|
|
(ert-deftest tp-test-remove ()
|
|
"Test tp-remove removes a specific property."
|
|
(tp-test-with-temp-buffer
|
|
(insert "Hello")
|
|
(tp-set 1 6 '(face bold help-echo "test"))
|
|
(should (eq (tp-at 1 'face) 'bold))
|
|
(tp-remove 1 6 'face)
|
|
(should (null (tp-at 1 'face)))
|
|
(should (equal (tp-at 1 'help-echo) "test"))))
|
|
|
|
(ert-deftest tp-test-clear ()
|
|
"Test tp-clear removes all properties."
|
|
(tp-test-with-temp-buffer
|
|
(insert "Hello World")
|
|
(tp-set 1 6 '(face bold))
|
|
(tp-set 7 12 '(face italic))
|
|
(tp-clear 1 12)
|
|
(should (null (tp-at 1 'face)))
|
|
(should (null (tp-at 7 'face)))))
|
|
|
|
(ert-deftest tp-test-clear-defaults-to-buffer ()
|
|
"Test tp-clear defaults to entire buffer."
|
|
(tp-test-with-temp-buffer
|
|
(insert "Hello World")
|
|
(tp-set 1 12 '(face bold))
|
|
(tp-clear)
|
|
(should (null (tp-at 1 'face)))
|
|
(should (null (tp-at 7 'face)))))
|
|
|
|
(ert-deftest tp-test-at ()
|
|
"Test tp-at returns all properties at point."
|
|
(tp-test-with-temp-buffer
|
|
(insert "Hello")
|
|
(tp-set 1 6 '(face bold help-echo "test"))
|
|
(let ((props (tp-at 1)))
|
|
(should (eq (plist-get props 'face) 'bold))
|
|
(should (equal (plist-get props 'help-echo) "test")))))
|
|
|
|
(ert-deftest tp-test-at-with-property ()
|
|
"Test tp-at returns specific property at point."
|
|
(tp-test-with-temp-buffer
|
|
(insert "Hello")
|
|
(tp-set 1 6 '(face bold help-echo "test"))
|
|
(should (eq (tp-at 1 'face) 'bold))
|
|
(should (equal (tp-at 1 'help-echo) "test"))
|
|
(should (null (tp-at 1 'mouse-face)))))
|
|
|
|
(ert-deftest tp-test-at-with-object ()
|
|
"Test tp-at with string object."
|
|
(let ((str (copy-sequence "Hello World")))
|
|
(tp-set 0 5 '(face bold help-echo "greeting") str)
|
|
;; All properties at position
|
|
(let ((props (tp-at 0 str)))
|
|
(should (eq (plist-get props 'face) 'bold))
|
|
(should (equal (plist-get props 'help-echo) "greeting")))
|
|
;; Specific property at position
|
|
(should (eq (tp-at 0 'face str) 'bold))
|
|
(should (equal (tp-at 0 'help-echo str) "greeting"))))
|
|
|
|
(ert-deftest tp-test-at-with-nested-path ()
|
|
"Test tp-at with nested property path."
|
|
(tp-test-with-temp-buffer
|
|
(insert "Hello")
|
|
(put-text-property 1 6 'face '(:foreground "red" :box (:color "blue" :line-width 2)))
|
|
(should (equal (tp-at 1 '(face :foreground)) "red"))
|
|
(should (equal (tp-at 1 '(face :box)) '(:color "blue" :line-width 2)))
|
|
(should (equal (tp-at 1 '(face :box :color)) "blue"))
|
|
(should (equal (tp-at 1 '(face :box :line-width)) 2))))
|
|
|
|
(ert-deftest tp-test-at-with-nested-path-on-string ()
|
|
"Test tp-at with nested property path on string."
|
|
(let ((str (copy-sequence "Hello World")))
|
|
(put-text-property 0 5 'face '(:foreground "red" :underline (:style wave)) str)
|
|
(should (equal (tp-at 0 '(face :foreground) str) "red"))
|
|
(should (equal (tp-at 0 '(face :underline :style) str) 'wave))))
|
|
|
|
(ert-deftest tp-test-at-defaults-to-point ()
|
|
"Test tp-at works with current point."
|
|
(tp-test-with-temp-buffer
|
|
(insert "Hello")
|
|
(tp-set 1 6 '(face bold))
|
|
(goto-char 3)
|
|
(should (eq (plist-get (tp-at (point)) 'face) 'bold))))
|
|
|
|
(ert-deftest tp-test-plist ()
|
|
"Test tp-plist merges properties from region."
|
|
(tp-test-with-temp-buffer
|
|
(insert "Hello World")
|
|
;; Put both properties on the same overlapping region for proper merging
|
|
(tp-set 1 12 '(face bold))
|
|
(tp-set 1 12 '(help-echo "test"))
|
|
(let ((props (tp-plist 1 12)))
|
|
(should (eq (plist-get props 'face) 'bold))
|
|
(should (equal (plist-get props 'help-echo) "test")))))
|
|
|
|
(ert-deftest tp-test-plist-on-string ()
|
|
"Test tp-plist works on entire string."
|
|
(let ((str (tp-set "Hello World" 'face 'bold 'help-echo "test")))
|
|
(let ((props (tp-plist str)))
|
|
(should (eq (plist-get props 'face) 'bold))
|
|
(should (equal (plist-get props 'help-echo) "test")))))
|
|
|
|
(ert-deftest tp-test-plist-on-string-range ()
|
|
"Test tp-plist works on string range with object parameter."
|
|
(let ((str (copy-sequence "Hello World")))
|
|
(tp-set 0 5 '(face bold) str)
|
|
(tp-set 6 11 '(help-echo "test") str)
|
|
(let ((props-start (tp-plist 0 5 str))
|
|
(props-end (tp-plist 6 11 str)))
|
|
(should (eq (plist-get props-start 'face) 'bold))
|
|
(should (equal (plist-get props-end 'help-echo) "test")))))
|
|
|
|
;;; ============================================================
|
|
;;; Text Property Interval Tests
|
|
;;; ============================================================
|
|
|
|
(ert-deftest tp-test-empty-p ()
|
|
"Test tp-empty-p detects empty properties."
|
|
(should (tp-empty-p "plain string"))
|
|
(should-not (tp-empty-p (propertize "styled" 'face 'bold))))
|
|
|
|
(ert-deftest tp-test-empty-p-with-nil ()
|
|
"Test tp-empty-p with nil (current buffer)."
|
|
(tp-test-with-temp-buffer
|
|
(insert "Hello World")
|
|
;; Empty buffer (no properties)
|
|
(should (tp-empty-p nil))
|
|
(should (tp-empty-p))
|
|
;; Add properties
|
|
(tp-set 1 6 '(face bold))
|
|
(should-not (tp-empty-p nil))
|
|
(should-not (tp-empty-p))))
|
|
|
|
(ert-deftest tp-test-empty-p-with-buffer ()
|
|
"Test tp-empty-p with explicit buffer object."
|
|
(tp-test-with-temp-buffer
|
|
(insert "Hello World")
|
|
(let ((buf (current-buffer)))
|
|
;; Empty (no properties)
|
|
(should (tp-empty-p buf))
|
|
;; Add properties
|
|
(tp-set 1 6 '(face bold))
|
|
(should-not (tp-empty-p buf)))))
|
|
|
|
(ert-deftest tp-test-intervals ()
|
|
"Test tp-intervals returns property intervals."
|
|
(tp-test-with-temp-buffer
|
|
(insert "Hello World")
|
|
(tp-set 1 6 '(face bold))
|
|
(tp-set 7 12 '(face italic))
|
|
(let ((intervals (tp-intervals 1 12)))
|
|
(should (>= (length intervals) 2)))))
|
|
|
|
;;; ============================================================
|
|
;;; Layer Definition Tests (using define-tp)
|
|
;;; ============================================================
|
|
|
|
(ert-deftest tp-test-layer-props ()
|
|
"Test tp-layer-props returns properties, and tp-name when requested."
|
|
(tp-test-with-temp-buffer
|
|
(define-tp my-layer ()
|
|
'(face bold))
|
|
;; Without tp-name (default for direct property setting)
|
|
(let ((props (tp-layer-props 'my-layer)))
|
|
(should (eq (plist-get props 'face) 'bold))
|
|
(should-not (plist-get props 'tp-name)))
|
|
;; With tp-name (for layer stack functions)
|
|
(let ((props (tp-layer-props 'my-layer t)))
|
|
(should (eq (plist-get props 'face) 'bold))
|
|
(should (eq (plist-get props 'tp-name) 'my-layer)))))
|
|
|
|
(ert-deftest tp-test-layer-props-returns-nil-for-undefined ()
|
|
"Test tp-layer-props returns nil for undefined layer."
|
|
(tp-test-with-temp-buffer
|
|
(should (null (tp-layer-props 'undefined-layer)))))
|
|
|
|
(ert-deftest tp-test-layer-undefine ()
|
|
"Test tp-undefine-layer removes layer definition."
|
|
(tp-test-with-temp-buffer
|
|
(define-tp test-layer ()
|
|
'(face bold))
|
|
(should (assoc 'test-layer tp-layer-alist))
|
|
(tp-undefine-layer 'test-layer)
|
|
(should-not (assoc 'test-layer tp-layer-alist))))
|
|
|
|
;;; ============================================================
|
|
;;; Layer Group Tests (using define-tps)
|
|
;;; ============================================================
|
|
|
|
(ert-deftest tp-test-group-props ()
|
|
"Test tp-group-props returns all layer properties."
|
|
(tp-test-with-temp-buffer
|
|
(define-tp layer1 ()
|
|
'(face bold))
|
|
(define-tp layer2 ()
|
|
'(face italic))
|
|
(define-tps my-group ()
|
|
'layer1
|
|
'layer2)
|
|
(let ((props-list (tp-group-props 'my-group)))
|
|
(should (= (length props-list) 2))
|
|
;; Check that both layers are present
|
|
(let ((faces (mapcar (lambda (p) (plist-get p 'face)) props-list)))
|
|
(should (memq 'bold faces))
|
|
(should (memq 'italic faces))))))
|
|
|
|
(ert-deftest tp-test-group-undefine ()
|
|
"Test tp-undefine-group removes group definition."
|
|
(tp-test-with-temp-buffer
|
|
(define-tp layer1 ()
|
|
'(face bold))
|
|
(define-tps my-group ()
|
|
'layer1)
|
|
(should (assoc 'my-group tp-layer-groups))
|
|
(tp-undefine-group 'my-group)
|
|
(should-not (assoc 'my-group tp-layer-groups))))
|
|
|
|
(ert-deftest tp-test-layer-reset ()
|
|
"Test tp-layer-reset clears all definitions."
|
|
(tp-test-with-temp-buffer
|
|
(define-tp layer1 ()
|
|
'(face bold))
|
|
(define-tp layer2 ()
|
|
'(face italic))
|
|
(define-tps group1 ()
|
|
'layer1
|
|
'layer2)
|
|
(should tp-layer-alist)
|
|
(should tp-layer-groups)
|
|
(tp-layer-reset)
|
|
(should-not tp-layer-alist)
|
|
(should-not tp-layer-groups)))
|
|
|
|
;;; ============================================================
|
|
;;; Layer Stack Operations Tests (New API)
|
|
;;; ============================================================
|
|
|
|
(ert-deftest tp-test-push-layer ()
|
|
"Test tp-push-layer adds layer to stack."
|
|
(tp-test-with-temp-buffer
|
|
(insert "Hello")
|
|
(define-tp layer1 () '(face bold))
|
|
(tp-push-layer 1 6 'layer1)
|
|
(should (eq (tp-at 1 'face) 'bold))
|
|
(should (eq (tp-at 1 'tp-name) 'layer1))))
|
|
|
|
(ert-deftest tp-test-push-layer-multiple ()
|
|
"Test pushing multiple layers."
|
|
(tp-test-with-temp-buffer
|
|
(insert "Hello")
|
|
(define-tp layer1 () '(face bold))
|
|
(define-tp layer2 () '(face italic))
|
|
(tp-push-layer 1 6 'layer1)
|
|
(tp-push-layer 1 6 'layer2)
|
|
;; layer2 should be on top (visible)
|
|
(should (eq (tp-at 1 'face) 'italic))
|
|
(should (eq (tp-at 1 'tp-name) 'layer2))
|
|
;; layer1 should be in the stack below
|
|
(should (tp-at 1 'tp-layers))))
|
|
|
|
(ert-deftest tp-test-delete-layer ()
|
|
"Test tp-delete-layer removes layer from stack."
|
|
(tp-test-with-temp-buffer
|
|
(insert "Hello")
|
|
(define-tp layer1 () '(face bold))
|
|
(define-tp layer2 () '(face italic))
|
|
(tp-push-layer 1 6 'layer1)
|
|
(tp-push-layer 1 6 'layer2)
|
|
;; Delete top layer
|
|
(tp-delete-layer 1 6 'layer2)
|
|
;; layer1 should now be visible
|
|
(should (eq (tp-at 1 'face) 'bold))
|
|
(should (eq (tp-at 1 'tp-name) 'layer1))))
|
|
|
|
(ert-deftest tp-test-delete-layer-from-middle ()
|
|
"Test deleting layer from middle of stack."
|
|
(tp-test-with-temp-buffer
|
|
(insert "Hello")
|
|
(define-tp layer1 () '(face bold))
|
|
(define-tp layer2 () '(face italic))
|
|
(define-tp layer3 () '(face underline))
|
|
(tp-push-layer 1 6 'layer1)
|
|
(tp-push-layer 1 6 'layer2)
|
|
(tp-push-layer 1 6 'layer3)
|
|
;; Delete middle layer
|
|
(tp-delete-layer 1 6 'layer2)
|
|
;; Top layer should still be visible
|
|
(should (eq (tp-at 1 'tp-name) 'layer3))
|
|
;; layer2 should not exist anymore
|
|
(should-not (tp-layer-exists-p 1 6 'layer2))))
|
|
|
|
(ert-deftest tp-test-pop-layer ()
|
|
"Test tp-pop-layer removes top layer."
|
|
(tp-test-with-temp-buffer
|
|
(insert "Hello")
|
|
(define-tp layer1 () '(face bold))
|
|
(define-tp layer2 () '(face italic))
|
|
(tp-push-layer 1 6 'layer1)
|
|
(tp-push-layer 1 6 'layer2)
|
|
;; Pop top layer
|
|
(tp-pop-layer 1 6)
|
|
;; layer1 should now be visible
|
|
(should (eq (tp-at 1 'tp-name) 'layer1))))
|
|
|
|
(ert-deftest tp-test-rotate-layer ()
|
|
"Test tp-rotate-layer cycles layers."
|
|
(tp-test-with-temp-buffer
|
|
(insert "Hello")
|
|
(define-tp layer1 () '(face bold))
|
|
(define-tp layer2 () '(face italic))
|
|
(define-tp layer3 () '(face underline))
|
|
(tp-push-layer 1 6 'layer1)
|
|
(tp-push-layer 1 6 'layer2)
|
|
(tp-push-layer 1 6 'layer3)
|
|
;; layer3 is on top
|
|
(should (eq (tp-layer-top 1 6) 'layer3))
|
|
;; Rotate once - layer2 should be on top
|
|
(tp-rotate-layer 1 6)
|
|
(should (eq (tp-layer-top 1 6) 'layer2))
|
|
;; Rotate again - layer1 should be on top
|
|
(tp-rotate-layer 1 6)
|
|
(should (eq (tp-layer-top 1 6) 'layer1))
|
|
;; Rotate again - layer3 should be on top (cycled back)
|
|
(tp-rotate-layer 1 6)
|
|
(should (eq (tp-layer-top 1 6) 'layer3))))
|
|
|
|
(ert-deftest tp-test-pin-layer ()
|
|
"Test tp-pin-layer brings layer to top."
|
|
(tp-test-with-temp-buffer
|
|
(insert "Hello")
|
|
(define-tp layer1 () '(face bold))
|
|
(define-tp layer2 () '(face italic))
|
|
(define-tp layer3 () '(face underline))
|
|
(tp-push-layer 1 6 'layer1)
|
|
(tp-push-layer 1 6 'layer2)
|
|
(tp-push-layer 1 6 'layer3)
|
|
;; Pin layer1 to top
|
|
(tp-pin-layer 1 6 'layer1)
|
|
(should (eq (tp-layer-top 1 6) 'layer1))))
|
|
|
|
(ert-deftest tp-test-raise-layer ()
|
|
"Test tp-raise-layer moves layer up."
|
|
(tp-test-with-temp-buffer
|
|
(insert "Hello")
|
|
(define-tp layer1 () '(face bold))
|
|
(define-tp layer2 () '(face italic))
|
|
(define-tp layer3 () '(face underline))
|
|
(tp-push-layer 1 6 'layer1)
|
|
(tp-push-layer 1 6 'layer2)
|
|
(tp-push-layer 1 6 'layer3)
|
|
;; layer3 is at idx 0, layer2 at 1, layer1 at 2
|
|
;; Raise layer1 by 2 (move to top)
|
|
(tp-raise-layer 1 6 'layer1 2)
|
|
(should (eq (tp-layer-top 1 6) 'layer1))))
|
|
|
|
(ert-deftest tp-test-switch-layer ()
|
|
"Test tp-switch-layer swaps two layers."
|
|
(tp-test-with-temp-buffer
|
|
(insert "Hello")
|
|
(define-tp layer1 () '(face bold))
|
|
(define-tp layer2 () '(face italic))
|
|
(tp-push-layer 1 6 'layer1)
|
|
(tp-push-layer 1 6 'layer2)
|
|
;; layer2 is on top
|
|
(should (eq (tp-layer-top 1 6) 'layer2))
|
|
;; Switch layer1 and layer2
|
|
(tp-switch-layer 1 6 'layer1 'layer2)
|
|
;; layer1 should now be on top
|
|
(should (eq (tp-layer-top 1 6) 'layer1))))
|
|
|
|
(ert-deftest tp-test-move-layer-by-index ()
|
|
"Test tp-move-layer moves layer by index."
|
|
(tp-test-with-temp-buffer
|
|
(insert "Hello")
|
|
(define-tp layer1 () '(face bold))
|
|
(define-tp layer2 () '(face italic))
|
|
(define-tp layer3 () '(face underline))
|
|
(tp-push-layer 1 6 'layer1)
|
|
(tp-push-layer 1 6 'layer2)
|
|
(tp-push-layer 1 6 'layer3)
|
|
;; Stack: layer3 (0), layer2 (1), layer1 (2)
|
|
(should (eq (tp-layer-top 1 6) 'layer3))
|
|
;; Move layer at index 2 (layer1) to index 0 (top)
|
|
(tp-move-layer 1 6 2 0)
|
|
;; layer1 should now be on top
|
|
(should (eq (tp-layer-top 1 6) 'layer1))))
|
|
|
|
(ert-deftest tp-test-move-layer-by-name ()
|
|
"Test tp-move-layer moves layer by name."
|
|
(tp-test-with-temp-buffer
|
|
(insert "Hello")
|
|
(define-tp layer1 () '(face bold))
|
|
(define-tp layer2 () '(face italic))
|
|
(define-tp layer3 () '(face underline))
|
|
(tp-push-layer 1 6 'layer1)
|
|
(tp-push-layer 1 6 'layer2)
|
|
(tp-push-layer 1 6 'layer3)
|
|
;; Stack: layer3 (0), layer2 (1), layer1 (2)
|
|
(should (eq (tp-layer-top 1 6) 'layer3))
|
|
;; Move layer1 to index 0 (top)
|
|
(tp-move-layer 1 6 'layer1 0)
|
|
;; layer1 should now be on top
|
|
(should (eq (tp-layer-top 1 6) 'layer1))))
|
|
|
|
(ert-deftest tp-test-move-layer-negative-index ()
|
|
"Test tp-move-layer with negative indices."
|
|
(tp-test-with-temp-buffer
|
|
(insert "Hello")
|
|
(define-tp layer1 () '(face bold))
|
|
(define-tp layer2 () '(face italic))
|
|
(define-tp layer3 () '(face underline))
|
|
(tp-push-layer 1 6 'layer1)
|
|
(tp-push-layer 1 6 'layer2)
|
|
(tp-push-layer 1 6 'layer3)
|
|
;; Stack: layer3 (0/-3), layer2 (1/-2), layer1 (2/-1)
|
|
(should (eq (tp-layer-top 1 6) 'layer3))
|
|
;; Move top layer (0) to bottom (-1)
|
|
(tp-move-layer 1 6 0 -1)
|
|
;; layer2 should now be on top
|
|
(should (eq (tp-layer-top 1 6) 'layer2))))
|
|
|
|
(ert-deftest tp-test-move-layer-on-string ()
|
|
"Test tp-move-layer works on strings."
|
|
(let ((str (copy-sequence "Hello")))
|
|
(setq tp-layer-alist nil)
|
|
(setq tp-layer-groups nil)
|
|
(define-tp layer1 () '(face bold))
|
|
(define-tp layer2 () '(face italic))
|
|
(tp-push-layer str 'layer1)
|
|
(tp-push-layer str 'layer2)
|
|
;; layer2 is on top
|
|
(should (eq (tp-at 0 'tp-name str) 'layer2))
|
|
;; Move layer1 to top
|
|
(tp-move-layer str 'layer1 0)
|
|
;; layer1 should now be on top
|
|
(should (eq (tp-at 0 'tp-name str) 'layer1))))
|
|
|
|
(ert-deftest tp-test-put-layer-at-idx ()
|
|
"Test tp-put-layer inserts layer at specified index."
|
|
(tp-test-with-temp-buffer
|
|
(insert "Hello")
|
|
(define-tp layer1 () '(face bold))
|
|
(define-tp layer2 () '(face italic))
|
|
(define-tp layer3 () '(face underline))
|
|
(tp-push-layer 1 6 'layer1)
|
|
(tp-push-layer 1 6 'layer2)
|
|
;; Insert layer3 at index 1 (between layer2 and layer1)
|
|
(tp-put-layer 1 6 'layer3 1)
|
|
;; layer2 should still be on top
|
|
(should (eq (tp-layer-top 1 6) 'layer2))
|
|
;; Should have 3 layers
|
|
(should (= (tp-layer-count 1 6) 3))))
|
|
|
|
(ert-deftest tp-test-merge-layers ()
|
|
"Test tp-merge-layers merges specified layers."
|
|
(tp-test-with-temp-buffer
|
|
(insert "Hello")
|
|
(define-tp layer1 () '(face bold))
|
|
(define-tp layer2 () '(help-echo "test"))
|
|
(tp-push-layer 1 6 'layer1)
|
|
(tp-push-layer 1 6 'layer2)
|
|
;; Merge layer1 and layer2 into merged-layer
|
|
(tp-merge-layers 1 6 'merged-layer '(layer1 layer2))
|
|
;; Should have 1 layer now
|
|
(should (= (tp-layer-count 1 6) 1))
|
|
;; The merged layer should have properties from both
|
|
(should (eq (tp-at 1 'tp-name) 'merged-layer))))
|
|
|
|
(ert-deftest tp-test-flatten-layers ()
|
|
"Test tp-flatten-layers flattens all layers."
|
|
(tp-test-with-temp-buffer
|
|
(insert "Hello")
|
|
(define-tp layer1 () '(face bold))
|
|
(define-tp layer2 () '(help-echo "test"))
|
|
(tp-push-layer 1 6 'layer1)
|
|
(tp-push-layer 1 6 'layer2)
|
|
;; Flatten all layers into flat-layer
|
|
(tp-flatten-layers 1 6 'flat-layer)
|
|
;; Should have 1 layer now
|
|
(should (= (tp-layer-count 1 6) 1))
|
|
(should (eq (tp-at 1 'tp-name) 'flat-layer))))
|
|
|
|
;;; ============================================================
|
|
;;; Layer Query Tests
|
|
;;; ============================================================
|
|
|
|
(ert-deftest tp-test-layer-list ()
|
|
"Test tp-layer-list returns all layer names."
|
|
(tp-test-with-temp-buffer
|
|
(insert "Hello")
|
|
(define-tp layer1 () '(face bold))
|
|
(define-tp layer2 () '(face italic))
|
|
(define-tp layer3 () '(face underline))
|
|
(tp-push-layer 1 6 'layer1)
|
|
(tp-push-layer 1 6 'layer2)
|
|
(tp-push-layer 1 6 'layer3)
|
|
(let ((layers (tp-layer-list 1 6)))
|
|
(should (= (length layers) 3))
|
|
(should (memq 'layer1 layers))
|
|
(should (memq 'layer2 layers))
|
|
(should (memq 'layer3 layers)))))
|
|
|
|
(ert-deftest tp-test-layer-count ()
|
|
"Test tp-layer-count returns correct count."
|
|
(tp-test-with-temp-buffer
|
|
(insert "Hello")
|
|
(define-tp layer1 () '(face bold))
|
|
(define-tp layer2 () '(face italic))
|
|
(tp-push-layer 1 6 'layer1)
|
|
(should (= (tp-layer-count 1 6) 1))
|
|
(tp-push-layer 1 6 'layer2)
|
|
(should (= (tp-layer-count 1 6) 2))))
|
|
|
|
(ert-deftest tp-test-layer-exists-p ()
|
|
"Test tp-layer-exists-p correctly detects layers."
|
|
(tp-test-with-temp-buffer
|
|
(insert "Hello")
|
|
(define-tp layer1 () '(face bold))
|
|
(tp-push-layer 1 6 'layer1)
|
|
(should (tp-layer-exists-p 1 6 'layer1))
|
|
(should-not (tp-layer-exists-p 1 6 'layer2))))
|
|
|
|
(ert-deftest tp-test-layer-top ()
|
|
"Test tp-layer-top returns top layer name."
|
|
(tp-test-with-temp-buffer
|
|
(insert "Hello")
|
|
(define-tp layer1 () '(face bold))
|
|
(define-tp layer2 () '(face italic))
|
|
(tp-push-layer 1 6 'layer1)
|
|
(should (eq (tp-layer-top 1 6) 'layer1))
|
|
(tp-push-layer 1 6 'layer2)
|
|
(should (eq (tp-layer-top 1 6) 'layer2))))
|
|
|
|
;;; ============================================================
|
|
;;; Match and Regexp Tests
|
|
;;; ============================================================
|
|
|
|
(ert-deftest tp-test-match-set ()
|
|
"Test tp-match-set sets properties on string matches."
|
|
(tp-test-with-temp-buffer
|
|
(insert "Hello World Hello")
|
|
(let ((regions (tp-match-set "Hello" '(face bold))))
|
|
(should (= (length regions) 2))
|
|
(should (eq (tp-at 1 'face) 'bold))
|
|
(should (eq (tp-at 13 'face) 'bold)))))
|
|
|
|
(ert-deftest tp-test-match-set-returns-regions ()
|
|
"Test tp-match-set returns correct region pairs."
|
|
(tp-test-with-temp-buffer
|
|
(insert "Hello World Hello")
|
|
(let ((regions (tp-match-set "Hello" nil)))
|
|
(should (= (length regions) 2))
|
|
(should (equal (car regions) '(1 . 6)))
|
|
(should (equal (cadr regions) '(13 . 18))))))
|
|
|
|
(ert-deftest tp-test-regexp-set ()
|
|
"Test tp-regexp-set sets properties on regexp matches."
|
|
(tp-test-with-temp-buffer
|
|
(insert "abc 123 def 456")
|
|
(let ((regions (tp-regexp-set "[0-9]+" '(face bold))))
|
|
(should (= (length regions) 2))
|
|
(should (eq (tp-at 5 'face) 'bold))
|
|
(should (eq (tp-at 13 'face) 'bold)))))
|
|
|
|
(ert-deftest tp-test-regexp-set-returns-regions ()
|
|
"Test tp-regexp-set returns correct region pairs."
|
|
(tp-test-with-temp-buffer
|
|
(insert "abc 123 def 456")
|
|
(let ((regions (tp-regexp-set "[0-9]+" nil)))
|
|
(should (= (length regions) 2)))))
|
|
|
|
;;; ============================================================
|
|
;;; Search and Navigation Tests
|
|
;;; ============================================================
|
|
|
|
(ert-deftest tp-test-forward ()
|
|
"Test tp-forward finds next property."
|
|
(tp-test-with-temp-buffer
|
|
(insert "Hello World")
|
|
(tp-set 7 12 '(face bold))
|
|
(goto-char 1)
|
|
(let ((match (tp-forward 'face)))
|
|
(should match)
|
|
(should (= (prop-match-beginning match) 7))
|
|
(should (= (prop-match-end match) 12)))))
|
|
|
|
(ert-deftest tp-test-forward-on-string ()
|
|
"Test tp-forward works on string objects."
|
|
(let ((str (copy-sequence "Hello World Hello")))
|
|
(tp-set 0 5 '(marker t) str)
|
|
(tp-set 12 17 '(marker t) str)
|
|
(let ((matches (tp-forward 'marker tp-any-value str 2)))
|
|
(should (= (length matches) 2))
|
|
(should (equal (car matches) '(0 5 t)))
|
|
(should (equal (cadr matches) '(12 17 t))))))
|
|
|
|
(ert-deftest tp-test-forward-with-n ()
|
|
"Test tp-forward with N parameter."
|
|
(tp-test-with-temp-buffer
|
|
(insert "Hello World Test Again")
|
|
(tp-set 1 6 '(face bold))
|
|
(tp-set 7 12 '(face italic))
|
|
(tp-set 13 17 '(face bold))
|
|
(goto-char 1)
|
|
;; Search twice should find third match
|
|
(let ((match (tp-forward 'face tp-any-value nil 2)))
|
|
(should match))))
|
|
|
|
(ert-deftest tp-test-backward ()
|
|
"Test tp-backward finds previous property."
|
|
(tp-test-with-temp-buffer
|
|
(insert "Hello World")
|
|
(tp-set 1 6 '(face bold))
|
|
(goto-char 12)
|
|
;; Explicit VALUE finds the previous region carrying that value.
|
|
(let ((match (tp-backward 'face 'bold)))
|
|
(should match)
|
|
(should (= (prop-match-beginning match) 1)))
|
|
;; Omitting VALUE matches any directly present property value.
|
|
(goto-char 12)
|
|
(let ((match (tp-backward 'face)))
|
|
(should match)
|
|
(should (= (prop-match-beginning match) 1))
|
|
(should (= (prop-match-end match) 6)))))
|
|
|
|
(ert-deftest tp-test-backward-on-string ()
|
|
"Test tp-backward works on string objects."
|
|
(let ((str (copy-sequence "Hello World Hello")))
|
|
(tp-set 0 5 '(marker t) str)
|
|
(tp-set 12 17 '(marker t) str)
|
|
(let ((matches (tp-backward 'marker tp-any-value str 2)))
|
|
(should (= (length matches) 2))
|
|
;; Backward returns matches in reverse order
|
|
(should (equal (car matches) '(12 17 t)))
|
|
(should (equal (cadr matches) '(0 5 t))))))
|
|
|
|
;;; tp-forward-do / tp-backward-do tests (new API)
|
|
|
|
(ert-deftest tp-test-forward-do-on-string ()
|
|
"Test tp-forward-do on string: applies function to the last match."
|
|
(let ((str (copy-sequence "hello World hello")))
|
|
(tp-set 0 5 '(marker t) str)
|
|
(tp-set 12 17 '(marker t) str)
|
|
;; Search 2 times, function only applied to the last match
|
|
(let ((count (tp-forward-do #'upcase 'marker tp-any-value str 2)))
|
|
(should (= count 2))
|
|
;; First match should NOT be upcased
|
|
(should (equal (substring str 0 5) "hello"))
|
|
;; Only the last (2nd) match should be upcased
|
|
(should (equal (substring str 12 17) "HELLO")))))
|
|
|
|
(ert-deftest tp-test-forward-do-on-string-with-range ()
|
|
"Test tp-forward-do on string with start/end range.
|
|
TIMES targets the TIMES-th match specifically; with only one match in
|
|
range, asking for the 2nd applies nothing (all-or-nothing, matching
|
|
the buffer path) and returns the available count."
|
|
(let ((str (copy-sequence "hello World hello")))
|
|
(tp-set 0 5 '(marker t) str)
|
|
(tp-set 12 17 '(marker t) str)
|
|
;; Search only in range 6-17 (after first match)
|
|
(let ((count
|
|
(tp-forward-do #'upcase 'marker tp-any-value str 2 6 17)))
|
|
(should (= count 1)) ; Only one match in range 6-17
|
|
;; First match should NOT be upcased
|
|
(should (equal (substring str 0 5) "hello"))
|
|
;; The requested 2nd match does not exist: nothing is applied
|
|
(should (equal (substring str 12 17) "hello")))))
|
|
|
|
(ert-deftest tp-test-forward-do-function-receives-start-end ()
|
|
"Test tp-forward-do passes start and end to function."
|
|
(let ((str (copy-sequence "hello World hello"))
|
|
(starts nil)
|
|
(ends nil))
|
|
(tp-set 0 5 '(marker t) str)
|
|
(tp-set 12 17 '(marker t) str)
|
|
;; Function accepts text, start, end
|
|
(let ((count (tp-forward-do (lambda (txt start end)
|
|
(push start starts)
|
|
(push end ends)
|
|
(upcase txt))
|
|
'marker tp-any-value str 2)))
|
|
(should (= count 2))
|
|
;; Only the last match positions were passed to function
|
|
(should (equal starts '(12)))
|
|
(should (equal ends '(17)))
|
|
;; Only the last match should be upcased
|
|
(should (equal (substring str 0 5) "hello"))
|
|
(should (equal (substring str 12 17) "HELLO")))))
|
|
|
|
(ert-deftest tp-test-forward-do-single-arg-function ()
|
|
"Test tp-forward-do with single-argument function."
|
|
(let ((str (copy-sequence "hello World hello")))
|
|
(tp-set 0 5 '(marker t) str)
|
|
(tp-set 12 17 '(marker t) str)
|
|
;; Use #'upcase which only takes one argument
|
|
(tp-forward-do #'upcase 'marker tp-any-value str 2)
|
|
;; Only the last match should be upcased
|
|
(should (equal (substring str 0 5) "hello"))
|
|
(should (equal (substring str 12 17) "HELLO"))))
|
|
|
|
(ert-deftest tp-test-backward-do-on-string ()
|
|
"Test tp-backward-do on string: applies function to the last match."
|
|
(let ((str (copy-sequence "hello World hello")))
|
|
(tp-set 0 5 '(marker t) str)
|
|
(tp-set 12 17 '(marker t) str)
|
|
;; Search backward 2 times, function only applied to the last match
|
|
(let ((count (tp-backward-do
|
|
#'upcase 'marker tp-any-value str 2)))
|
|
(should (= count 2))
|
|
;; Only the last (2nd) match should be upcased (first in order)
|
|
(should (equal (substring str 0 5) "HELLO"))
|
|
;; First match (searched backward) should NOT be upcased
|
|
(should (equal (substring str 12 17) "hello")))))
|
|
|
|
(ert-deftest tp-test-backward-do-on-string-with-range ()
|
|
"Test tp-backward-do on string with start/end range.
|
|
All-or-nothing: with one match in range, requesting the 2nd applies
|
|
nothing and returns the available count."
|
|
(let ((str (copy-sequence "hello World hello")))
|
|
(tp-set 0 5 '(marker t) str)
|
|
(tp-set 12 17 '(marker t) str)
|
|
;; Search only in range 0-10 (before second match)
|
|
(let ((count
|
|
(tp-backward-do #'upcase 'marker tp-any-value str 2 0 10)))
|
|
(should (= count 1)) ; Only one match in range 0-10
|
|
;; The requested 2nd match does not exist: nothing is applied
|
|
(should (equal (substring str 0 5) "hello"))
|
|
(should (equal (substring str 12 17) "hello")))))
|
|
|
|
(ert-deftest tp-test-backward-do-function-receives-start-end ()
|
|
"Test tp-backward-do passes start and end to function."
|
|
(let ((str (copy-sequence "hello World hello"))
|
|
(starts nil)
|
|
(ends nil))
|
|
(tp-set 0 5 '(marker t) str)
|
|
(tp-set 12 17 '(marker t) str)
|
|
;; Function accepts text, start, end
|
|
(let ((count (tp-backward-do (lambda (txt start end)
|
|
(push start starts)
|
|
(push end ends)
|
|
(upcase txt))
|
|
'marker tp-any-value str 2)))
|
|
(should (= count 2))
|
|
;; Only the last match positions were passed to function
|
|
(should (equal starts '(0)))
|
|
(should (equal ends '(5)))
|
|
;; Only the last match should be upcased
|
|
(should (equal (substring str 0 5) "HELLO"))
|
|
(should (equal (substring str 12 17) "hello")))))
|
|
|
|
(ert-deftest tp-test-backward-do-single-arg-function ()
|
|
"Test tp-backward-do with single-argument function."
|
|
(let ((str (copy-sequence "hello World hello")))
|
|
(tp-set 0 5 '(marker t) str)
|
|
(tp-set 12 17 '(marker t) str)
|
|
;; Use #'upcase which only takes one argument
|
|
(tp-backward-do #'upcase 'marker tp-any-value str 2)
|
|
;; Only the last match should be upcased
|
|
(should (equal (substring str 0 5) "HELLO"))
|
|
(should (equal (substring str 12 17) "hello"))))
|
|
|
|
(ert-deftest tp-test-search-on-string ()
|
|
"Test tp-search finds all matching properties in a string."
|
|
(let ((str (copy-sequence "Hello World Hello")))
|
|
(tp-set 0 5 '(marker t) str)
|
|
(tp-set 12 17 '(marker t) str)
|
|
(let ((matches (tp-search str 'marker)))
|
|
(should (= (length matches) 2))
|
|
(should (equal (car matches) '(0 5 t)))
|
|
(should (equal (cadr matches) '(12 17 t))))))
|
|
|
|
(ert-deftest tp-test-search-on-string-with-value ()
|
|
"Test tp-search filters by value in a string."
|
|
(let ((str (copy-sequence "Hello World Hello")))
|
|
(tp-set 0 5 '(type heading) str)
|
|
(tp-set 6 11 '(type paragraph) str)
|
|
(tp-set 12 17 '(type heading) str)
|
|
(let ((matches (tp-search str 'type 'heading)))
|
|
(should (= (length matches) 2))
|
|
(should (equal (caddr (car matches)) 'heading))
|
|
(should (equal (caddr (cadr matches)) 'heading)))))
|
|
|
|
(ert-deftest tp-test-search-in-range ()
|
|
"Test tp-search finds all matching properties in a buffer range."
|
|
(tp-test-with-temp-buffer
|
|
(insert "Hello World Hello")
|
|
(tp-set 1 6 '(marker t))
|
|
(tp-set 13 18 '(marker t))
|
|
(let ((matches (tp-search 1 18 'marker)))
|
|
(should (= (length matches) 2))
|
|
(should (equal (car matches) '(1 6 t)))
|
|
(should (equal (cadr matches) '(13 18 t))))))
|
|
|
|
(ert-deftest tp-test--search-do-on-string ()
|
|
"Test tp--search-do applies function to all matches in a string (internal API)."
|
|
(let ((str (copy-sequence "Hello World Hello")))
|
|
(tp-set 0 5 '(marker t) str)
|
|
(tp-set 12 17 '(marker t) str)
|
|
(let ((result nil))
|
|
(tp--search-do
|
|
(lambda (match _obj)
|
|
(push (car match) result))
|
|
'marker tp-any-value str)
|
|
(should (= (length result) 2))
|
|
(should (member 0 result))
|
|
(should (member 12 result)))))
|
|
|
|
(ert-deftest tp-test--search-do-in-range ()
|
|
"Test tp--search-do applies function to all matches in a buffer range (internal API)."
|
|
(tp-test-with-temp-buffer
|
|
(insert "Hello World Hello")
|
|
(tp-set 1 6 '(marker t))
|
|
(tp-set 13 18 '(marker t))
|
|
(let ((result nil))
|
|
(tp--search-do
|
|
(lambda (match _obj)
|
|
(push (car match) result))
|
|
'marker tp-any-value nil 1 18)
|
|
(should (= (length result) 2))
|
|
(should (member 1 result))
|
|
(should (member 13 result)))))
|
|
|
|
(ert-deftest tp-test-search-map-on-string ()
|
|
"Test tp-search-map applies function to matched text in a string."
|
|
(let ((str (copy-sequence "hello World hello")))
|
|
(tp-set 0 5 '(marker t) str)
|
|
(tp-set 12 17 '(marker t) str)
|
|
(let ((count (tp-search-map #'upcase 'marker tp-any-value str)))
|
|
(should (= count 2))
|
|
;; Check that text was upcased
|
|
(should (equal (substring str 0 5) "HELLO"))
|
|
(should (equal (substring str 12 17) "HELLO")))))
|
|
|
|
(ert-deftest tp-test-search-map-in-range ()
|
|
"Test tp-search-map applies function to matched text in a buffer range."
|
|
(tp-test-with-temp-buffer
|
|
(insert "hello World hello")
|
|
(tp-set 1 6 '(marker t))
|
|
(tp-set 13 18 '(marker t))
|
|
(let ((count
|
|
(tp-search-map #'upcase 'marker tp-any-value nil 1 18)))
|
|
(should (= count 2))
|
|
;; Check that text was upcased
|
|
(should (equal (buffer-substring 1 6) "HELLO"))
|
|
(should (equal (buffer-substring 13 18) "HELLO")))))
|
|
|
|
(ert-deftest tp-test-search-map-property-modification ()
|
|
"Test tp-search-map applies property modifications to matched text."
|
|
(let ((str (copy-sequence "hello World hello")))
|
|
(tp-set 0 5 '(marker t) str)
|
|
(tp-set 12 17 '(marker t) str)
|
|
;; First upcase the text
|
|
(tp-search-map #'upcase 'marker tp-any-value str)
|
|
;; Then add face property
|
|
(tp-search-map (lambda (txt)
|
|
(tp-add txt 'face '(:background "orange")))
|
|
'marker tp-any-value str)
|
|
;; Check text was upcased
|
|
(should (equal (substring str 0 5) "HELLO"))
|
|
(should (equal (substring str 12 17) "HELLO"))
|
|
;; Check face property was added
|
|
(let ((props-0 (text-properties-at 0 str))
|
|
(props-12 (text-properties-at 12 str)))
|
|
(should (equal (plist-get (plist-get props-0 'face) :background) "orange"))
|
|
(should (equal (plist-get (plist-get props-12 'face) :background) "orange")))))
|
|
|
|
(ert-deftest tp-test-search-map-with-start-end-idx ()
|
|
"Test tp-search-map passes start, end, and index to function."
|
|
(let ((str (copy-sequence "aaa bbb ccc"))
|
|
(positions nil))
|
|
(tp-set 0 3 '(marker t) str)
|
|
(tp-set 4 7 '(marker t) str)
|
|
(tp-set 8 11 '(marker t) str)
|
|
;; Use a function that accepts text, start, end, idx
|
|
(tp-search-map (lambda (txt start end idx)
|
|
(push (list start end idx) positions)
|
|
(upcase txt))
|
|
'marker tp-any-value str)
|
|
;; Check positions and indices were passed in order (reversed due to push)
|
|
(should (equal (reverse positions) '((0 3 0) (4 7 1) (8 11 2))))
|
|
;; Check text was transformed (uppercased)
|
|
(should (equal (substring str 0 3) "AAA"))
|
|
(should (equal (substring str 4 7) "BBB"))
|
|
(should (equal (substring str 8 11) "CCC"))))
|
|
|
|
(ert-deftest tp-test-search-map-with-start-end-in-buffer ()
|
|
"Test tp-search-map passes start and end to function in buffer range."
|
|
(tp-test-with-temp-buffer
|
|
(insert "aaa bbb ccc")
|
|
(tp-set 1 4 '(marker t))
|
|
(tp-set 5 8 '(marker t))
|
|
(tp-set 9 12 '(marker t))
|
|
(let ((positions nil))
|
|
(tp-search-map (lambda (_txt start end idx)
|
|
(push (list start end idx) positions)
|
|
(format "[%d]" idx))
|
|
'marker tp-any-value nil 1 12)
|
|
;; Check positions and indices were passed in order
|
|
(should (equal (reverse positions) '((1 4 0) (5 8 1) (9 12 2))))
|
|
;; Check text was replaced with index markers
|
|
(should (string-match-p "\\[0\\]" (buffer-string)))
|
|
(should (string-match-p "\\[1\\]" (buffer-string)))
|
|
(should (string-match-p "\\[2\\]" (buffer-string))))))
|
|
|
|
(ert-deftest tp-test-search-map-backward-compat ()
|
|
"Test tp-search-map still works with single-argument functions."
|
|
(let ((str (copy-sequence "hello world")))
|
|
(tp-set 0 5 '(marker t) str)
|
|
;; Use #'upcase which only takes one argument
|
|
(tp-search-map #'upcase 'marker tp-any-value str)
|
|
(should (equal (substring str 0 5) "HELLO"))))
|
|
|
|
(ert-deftest tp-test-search-map-with-range ()
|
|
"Test tp-search-map with start and end range."
|
|
(let ((str (copy-sequence "hello World hello")))
|
|
(tp-set 0 5 '(marker t) str)
|
|
(tp-set 12 17 '(marker t) str)
|
|
;; Only search in range 0-10 (first match only)
|
|
(let ((count
|
|
(tp-search-map #'upcase 'marker tp-any-value str 0 10)))
|
|
(should (= count 1))
|
|
;; First match should be upcased
|
|
(should (equal (substring str 0 5) "HELLO"))
|
|
;; Second match should NOT be upcased
|
|
(should (equal (substring str 12 17) "hello")))))
|
|
|
|
;;; ============================================================
|
|
;;; Utility Function Tests
|
|
;;; ============================================================
|
|
|
|
;; Tests for tp-search are in Search and Navigation Tests section above
|
|
|
|
;;; ============================================================
|
|
;;; Edge Case Tests
|
|
;;; ============================================================
|
|
|
|
(ert-deftest tp-test-empty-region ()
|
|
"Test operations on empty buffer."
|
|
(tp-test-with-temp-buffer
|
|
(should (null (tp-at 1)))
|
|
(should (tp-empty-p))))
|
|
|
|
(ert-deftest tp-test-overlapping-regions ()
|
|
"Test overlapping property regions."
|
|
(tp-test-with-temp-buffer
|
|
(insert "Hello World")
|
|
(tp-set 1 8 '(prop1 val1))
|
|
(tp-set 5 12 '(prop2 val2))
|
|
(should (eq (tp-at 1 'prop1) 'val1))
|
|
(should (null (tp-at 1 'prop2)))
|
|
(should (eq (tp-at 6 'prop1) 'val1))
|
|
(should (eq (tp-at 6 'prop2) 'val2))
|
|
(should (null (tp-at 10 'prop1)))
|
|
(should (eq (tp-at 10 'prop2) 'val2))))
|
|
|
|
(ert-deftest tp-test-single-char-region ()
|
|
"Test operations on single character."
|
|
(tp-test-with-temp-buffer
|
|
(insert "H")
|
|
(tp-set 1 2 '(face bold))
|
|
(should (eq (tp-at 1 'face) 'bold))))
|
|
|
|
(ert-deftest tp-test-layer-on-string ()
|
|
"Test layer operations on string object."
|
|
(let ((str (copy-sequence "Hello")))
|
|
(set-text-properties 0 5 nil str)
|
|
(should (tp-empty-p str))))
|
|
|
|
;;; ============================================================
|
|
;;; Object Parameter Support Tests
|
|
;;; ============================================================
|
|
|
|
(ert-deftest tp-test-put-on-string ()
|
|
"Test tp-set works on string objects."
|
|
(let ((str (copy-sequence "Hello World")))
|
|
(tp-set 0 5 '(face bold) str)
|
|
(should (eq (get-text-property 0 'face str) 'bold))
|
|
(should (null (get-text-property 6 'face str)))))
|
|
|
|
(ert-deftest tp-test-put-on-string-returns-string ()
|
|
"Test tp-set returns the modified string."
|
|
(let* ((str (copy-sequence "Hello"))
|
|
(result (tp-set 0 5 '(face bold) str)))
|
|
(should (stringp result))
|
|
(should (eq (get-text-property 0 'face result) 'bold))))
|
|
|
|
(ert-deftest tp-test-put-entire-string ()
|
|
"Test tp-set applies to entire string with flat properties."
|
|
(let* ((str (copy-sequence "Hello"))
|
|
(result (tp-set str 'face 'bold 'help-echo "test")))
|
|
(should (stringp result))
|
|
(should (eq (get-text-property 0 'face result) 'bold))
|
|
(should (equal (get-text-property 0 'help-echo result) "test"))
|
|
(should (eq (get-text-property 4 'face result) 'bold))))
|
|
|
|
(ert-deftest tp-test-match-set-on-string ()
|
|
"Test tp-match-set works on string objects."
|
|
(let* ((str (copy-sequence "Hello World Hello"))
|
|
(result (tp-match-set "Hello" '(face bold) str)))
|
|
(should (stringp result))
|
|
(should (eq (get-text-property 0 'face result) 'bold))
|
|
(should (eq (get-text-property 12 'face result) 'bold))
|
|
(should (null (get-text-property 6 'face result)))))
|
|
|
|
(ert-deftest tp-test-regexp-set-on-string ()
|
|
"Test tp-regexp-set works on string objects."
|
|
(let* ((str (copy-sequence "abc 123 def 456"))
|
|
(result (tp-regexp-set "[0-9]+" '(face bold) str)))
|
|
(should (stringp result))
|
|
(should (eq (get-text-property 4 'face result) 'bold))
|
|
(should (eq (get-text-property 12 'face result) 'bold))
|
|
(should (null (get-text-property 0 'face result)))))
|
|
|
|
;;; ============================================================
|
|
;;; Enhanced tp-get Tests
|
|
;;; ============================================================
|
|
|
|
(ert-deftest tp-test-get-single-position ()
|
|
"Test tp-get with single position."
|
|
(tp-test-with-temp-buffer
|
|
(insert "Hello")
|
|
(tp-set 1 6 '(face bold))
|
|
(should (eq (tp-at 1 'face) 'bold))
|
|
(should (eq (tp-at 3 'face) 'bold))))
|
|
|
|
(ert-deftest tp-test-get-range-property ()
|
|
"Test tp-get with range and specific property.
|
|
Returns list of (START END VALUE) intervals."
|
|
(tp-test-with-temp-buffer
|
|
(insert "Hello World")
|
|
(tp-set 1 6 '(face bold))
|
|
(should (equal (tp-get 1 6 'face) '((1 6 bold))))
|
|
(should (null (tp-get 7 12 'face)))))
|
|
|
|
(ert-deftest tp-test-get-range-all-properties ()
|
|
"Test tp-get with range returns all property intervals."
|
|
(tp-test-with-temp-buffer
|
|
(insert "Hello World")
|
|
(tp-set 1 6 '(face bold help-echo "test"))
|
|
(let ((intervals (tp-get 1 6)))
|
|
(should (= (length intervals) 1))
|
|
(let ((props (caddr (car intervals))))
|
|
(should (eq (plist-get props 'face) 'bold))
|
|
(should (equal (plist-get props 'help-echo) "test"))))))
|
|
|
|
(ert-deftest tp-test-get-range-on-string ()
|
|
"Test tp-get with range on string object.
|
|
Returns list of (START END VALUE) intervals."
|
|
(let ((str (copy-sequence "Hello World")))
|
|
(tp-set 0 5 '(face bold) str)
|
|
(should (equal (tp-get 0 5 'face str) '((0 5 bold))))
|
|
(should (null (tp-get 6 11 'face str)))))
|
|
|
|
;;; ============================================================
|
|
;;; New API Tests (tp-reset, tp-set, tp-set-face, tp-set-display, tp-add)
|
|
;;; ============================================================
|
|
|
|
(ert-deftest tp-test-reset ()
|
|
"Test tp-reset completely replaces all properties."
|
|
(tp-test-with-temp-buffer
|
|
(insert "Hello")
|
|
(tp-set 1 6 '(face bold help-echo "test"))
|
|
;; tp-reset should completely replace
|
|
(tp-reset 1 6 '(mouse-face highlight))
|
|
(should (eq (tp-at 1 'mouse-face) 'highlight))
|
|
(should (null (tp-at 1 'face)))
|
|
(should (null (tp-at 1 'help-echo)))))
|
|
|
|
(ert-deftest tp-test-reset-on-string ()
|
|
"Test tp-reset on string."
|
|
(let ((str (copy-sequence "Hello World")))
|
|
(tp-set 0 5 '(face bold help-echo "test") str)
|
|
(tp-reset 0 5 '(mouse-face highlight) str)
|
|
(should (eq (get-text-property 0 'mouse-face str) 'highlight))
|
|
(should (null (get-text-property 0 'face str)))))
|
|
|
|
(ert-deftest tp-test-reset-entire-string ()
|
|
"Test tp-reset on entire string."
|
|
(let* ((str (tp-set "Hello" 'face 'bold 'help-echo "test"))
|
|
(result (tp-reset str 'mouse-face 'highlight)))
|
|
(should (eq (get-text-property 0 'mouse-face result) 'highlight))
|
|
(should (null (get-text-property 0 'face result)))))
|
|
|
|
(ert-deftest tp-test-set-preserves-other-properties ()
|
|
"Test tp-set preserves unspecified properties."
|
|
(tp-test-with-temp-buffer
|
|
(insert "Hello")
|
|
(tp-set 1 6 '(face bold help-echo "test"))
|
|
;; tp-set should only replace specified properties
|
|
(tp-set 1 6 '(face italic))
|
|
(should (eq (tp-at 1 'face) 'italic))
|
|
(should (equal (tp-at 1 'help-echo) "test"))))
|
|
|
|
(ert-deftest tp-test-add ()
|
|
"Test tp-add adds/updates properties without replacing."
|
|
(tp-test-with-temp-buffer
|
|
(insert "Hello")
|
|
(tp-set 1 6 '(face bold help-echo "test"))
|
|
(tp-add 1 6 '(mouse-face highlight))
|
|
(should (eq (tp-at 1 'face) 'bold))
|
|
(should (equal (tp-at 1 'help-echo) "test"))
|
|
(should (eq (tp-at 1 'mouse-face) 'highlight))))
|
|
|
|
(ert-deftest tp-test-add-deep-merge ()
|
|
"Test tp-add deeply merges nested properties."
|
|
(tp-test-with-temp-buffer
|
|
(insert "Hello")
|
|
(tp-set 1 6 '(face (:foreground "red" :weight bold)))
|
|
(tp-add 1 6 '(face (:background "blue")))
|
|
(let ((face (tp-at 1 'face)))
|
|
(should (equal (plist-get face :foreground) "red"))
|
|
(should (eq (plist-get face :weight) 'bold))
|
|
(should (equal (plist-get face :background) "blue")))))
|
|
|
|
(ert-deftest tp-test-add-face-subprop-override ()
|
|
"Test tp-add correctly merges face sub-properties.
|
|
Later values should override earlier values for the same sub-property."
|
|
;; The original issue: (tp-add (tp-add (tp-set \"emacs\" 'face 'bold)
|
|
;; 'face '(:foreground \"red\")) 'face '(bold (:foreground \"green\")))
|
|
;; should result in :foreground \"green\", not both \"red\" and \"green\"
|
|
(let* ((base (tp-set "emacs" 'face 'bold))
|
|
(with-red (tp-add base 'face '(:foreground "red")))
|
|
(with-green (tp-add with-red 'face '(bold (:foreground "green")))))
|
|
;; Final result should have only one :foreground which is "green"
|
|
(let ((face3 (get-text-property 0 'face with-green)))
|
|
(should (listp face3))
|
|
(should (member 'bold face3))
|
|
;; Extract the plist part
|
|
(let ((plist-part (cl-find-if (lambda (f)
|
|
(and (listp f) (keywordp (car-safe f))))
|
|
face3)))
|
|
(should plist-part)
|
|
(should (equal (plist-get plist-part :foreground) "green"))
|
|
;; Ensure there's no duplicate :foreground
|
|
(let ((plist-count (cl-count-if (lambda (f)
|
|
(and (listp f) (keywordp (car-safe f))))
|
|
face3)))
|
|
(should (= plist-count 1)))))))
|
|
|
|
(ert-deftest tp-test-add-on-string ()
|
|
"Test tp-add on string."
|
|
(let ((str (copy-sequence "Hello")))
|
|
(tp-set 0 5 '(face bold) str)
|
|
(tp-add 0 5 '(help-echo "test") str)
|
|
(should (eq (get-text-property 0 'face str) 'bold))
|
|
(should (equal (get-text-property 0 'help-echo str) "test"))))
|
|
|
|
;;; ============================================================
|
|
;;; Enhanced tp-at Tests
|
|
;;; ============================================================
|
|
|
|
(ert-deftest tp-test-at-nested-sub-property ()
|
|
"Test tp-at with nested sub-properties."
|
|
(tp-test-with-temp-buffer
|
|
(insert "Hello")
|
|
(put-text-property 1 6 'face '(:foreground "red" :box (:color "blue" :line-width 2)))
|
|
(should (equal (tp-at 1 '(face :foreground)) "red"))
|
|
(should (equal (tp-at 1 '(face :box :color)) "blue"))
|
|
(should (equal (tp-at 1 '(face :box :line-width)) 2))))
|
|
|
|
(ert-deftest tp-test-at-display-sub-property ()
|
|
"Test tp-at with display sub-properties that are plists."
|
|
(tp-test-with-temp-buffer
|
|
(insert "Hello")
|
|
;; Use a plist-style display property
|
|
(put-text-property 1 6 'display '(:height 1.5 :width 10))
|
|
(should (equal (tp-at 1 '(display :height)) 1.5))
|
|
(should (equal (tp-at 1 '(display :width)) 10))))
|
|
|
|
;;; ============================================================
|
|
;;; Enhanced tp-remove Tests
|
|
;;; ============================================================
|
|
|
|
(ert-deftest tp-test-remove-sub-property-with-path ()
|
|
"Test tp-remove with sub-property path."
|
|
(tp-test-with-temp-buffer
|
|
(insert "Hello")
|
|
(put-text-property 1 6 'face '(:foreground "red" :underline (:style wave :color "blue")))
|
|
;; Remove just :underline from face
|
|
(tp-remove 1 6 '(face :underline))
|
|
(let ((face (tp-at 1 'face)))
|
|
(should (equal (plist-get face :foreground) "red"))
|
|
(should (null (plist-get face :underline))))))
|
|
|
|
(ert-deftest tp-test-remove-nested-sub-properties ()
|
|
"Test tp-remove with nested sub-properties."
|
|
(tp-test-with-temp-buffer
|
|
(insert "Hello")
|
|
(put-text-property 1 6 'face '(:foreground "red" :underline (:style wave :position t :color "blue")))
|
|
;; Remove :style and :position from :underline, keep :color
|
|
(tp-remove 1 6 '(face :underline (:style :position)))
|
|
(let* ((face (tp-at 1 'face))
|
|
(underline (plist-get face :underline)))
|
|
(should (equal (plist-get face :foreground) "red"))
|
|
(should (equal (plist-get underline :color) "blue"))
|
|
(should (null (plist-get underline :style)))
|
|
(should (null (plist-get underline :position))))))
|
|
|
|
;;; ============================================================
|
|
;;; Match Pattern Format Tests
|
|
;;; ============================================================
|
|
|
|
(ert-deftest tp-test-match-set-multiple-patterns ()
|
|
"Test tp-match-set with multiple patterns (list of patterns)."
|
|
(tp-test-with-temp-buffer
|
|
(insert "Hello world, Hello again")
|
|
;; Match both "world" and "Hello" - both should get properties applied
|
|
(let ((regions (tp-match-set '("world" "Hello") '(face bold))))
|
|
;; Should find 3 matches: "Hello", "world", "Hello"
|
|
(should (= (length regions) 3))
|
|
;; Check that "Hello" at position 1 has face bold
|
|
(should (eq (tp-at 1 'face) 'bold))
|
|
;; Check that "world" at position 7 has face bold
|
|
(should (eq (tp-at 7 'face) 'bold))
|
|
;; Check that "Hello" at position 14 has face bold
|
|
(should (eq (tp-at 14 'face) 'bold)))))
|
|
|
|
(ert-deftest tp-test-match-set-multiple-patterns-on-string ()
|
|
"Test tp-match-set with multiple patterns on string."
|
|
(let* ((str (copy-sequence "Hello world, Hello again"))
|
|
(result (tp-match-set '("world" "Hello") '(face bold) str)))
|
|
(should (stringp result))
|
|
;; Check that "Hello" at position 0 has face bold
|
|
(should (eq (get-text-property 0 'face result) 'bold))
|
|
;; Check that "world" at position 6 has face bold
|
|
(should (eq (get-text-property 6 'face result) 'bold))
|
|
;; Check that "Hello" at position 13 has face bold
|
|
(should (eq (get-text-property 13 'face result) 'bold))))
|
|
|
|
(ert-deftest tp-test-match-reset ()
|
|
"Test tp-match-reset completely replaces properties."
|
|
(tp-test-with-temp-buffer
|
|
(insert "Hello World Hello")
|
|
(tp-set 1 6 '(help-echo "original"))
|
|
(tp-match-reset "Hello" '(face bold))
|
|
(should (eq (tp-at 1 'face) 'bold))
|
|
;; Properties should be completely replaced
|
|
(should (null (tp-at 1 'help-echo)))))
|
|
|
|
(ert-deftest tp-test-match-add ()
|
|
"Test tp-match-add adds/updates properties."
|
|
(tp-test-with-temp-buffer
|
|
(insert "Hello World Hello")
|
|
(tp-set 1 6 '(help-echo "original"))
|
|
(tp-match-add "Hello" '(face bold))
|
|
(should (eq (tp-at 1 'face) 'bold))
|
|
;; Original properties should be preserved
|
|
(should (equal (tp-at 1 'help-echo) "original"))))
|
|
|
|
(ert-deftest tp-test-regexp-reset ()
|
|
"Test tp-regexp-reset completely replaces properties."
|
|
(tp-test-with-temp-buffer
|
|
(insert "abc 123 def 456")
|
|
(tp-set 5 8 '(help-echo "original"))
|
|
(tp-regexp-reset "[0-9]+" '(face bold))
|
|
(should (eq (tp-at 5 'face) 'bold))
|
|
;; Properties should be completely replaced
|
|
(should (null (tp-at 5 'help-echo)))))
|
|
|
|
(ert-deftest tp-test-regexp-add ()
|
|
"Test tp-regexp-add adds/updates properties."
|
|
(tp-test-with-temp-buffer
|
|
(insert "abc 123 def 456")
|
|
(tp-set 5 8 '(help-echo "original"))
|
|
(tp-regexp-add "[0-9]+" '(face bold))
|
|
(should (eq (tp-at 5 'face) 'bold))
|
|
;; Original properties should be preserved
|
|
(should (equal (tp-at 5 'help-echo) "original"))))
|
|
|
|
(ert-deftest tp-test-match-reset-on-string ()
|
|
"Test tp-match-reset on string."
|
|
(let* ((str (copy-sequence "Hello World Hello"))
|
|
(result (tp-match-reset "Hello" '(face bold) str)))
|
|
(should (eq (get-text-property 0 'face result) 'bold))
|
|
(should (eq (get-text-property 12 'face result) 'bold))))
|
|
|
|
(ert-deftest tp-test-regexp-add-on-string ()
|
|
"Test tp-regexp-add on string.
|
|
For strings, returns a NEW string (original is not modified)."
|
|
(let ((str (copy-sequence "abc 123 def 456")))
|
|
(tp-set 4 7 '(help-echo "original") str)
|
|
(let ((result (tp-regexp-add "[0-9]+" '(face bold) str)))
|
|
;; Result should have both properties (face added, help-echo preserved)
|
|
(should (eq (get-text-property 4 'face result) 'bold))
|
|
(should (equal (get-text-property 4 'help-echo result) "original"))
|
|
;; Original should NOT have face property added by tp-regexp-add
|
|
(should (null (get-text-property 4 'face str))))))
|
|
|
|
(ert-deftest tp-test-match-set-string-as-last-arg ()
|
|
"Test tp-match-set with string as last argument."
|
|
(let ((str (copy-sequence "Hello World Hello")))
|
|
(let ((result (tp-match-set "Hello" '(face bold) str)))
|
|
(should (stringp result))
|
|
(should (eq (get-text-property 0 'face result) 'bold))
|
|
(should (eq (get-text-property 12 'face result) 'bold))
|
|
(should (null (get-text-property 6 'face result))))))
|
|
|
|
(ert-deftest tp-test-regexp-set-string-as-last-arg ()
|
|
"Test tp-regexp-set with string as last argument."
|
|
(let ((str (copy-sequence "abc 123 def 456")))
|
|
(let ((result (tp-regexp-set "[0-9]+" '(face italic) str)))
|
|
(should (stringp result))
|
|
(should (eq (get-text-property 4 'face result) 'italic))
|
|
(should (eq (get-text-property 12 'face result) 'italic))
|
|
(should (null (get-text-property 0 'face result))))))
|
|
|
|
(ert-deftest tp-test-regexp-set-multiple-patterns ()
|
|
"Test tp-regexp-set with multiple patterns (list of regexps)."
|
|
(tp-test-with-temp-buffer
|
|
(insert "abc 123 def 456 ghi")
|
|
;; Match both numbers and "abc" - all should get properties applied
|
|
(let ((regions (tp-regexp-set '("[0-9]+" "abc") '(face bold))))
|
|
;; Should find 3 matches: "abc", "123", "456"
|
|
(should (= (length regions) 3))
|
|
;; Check that "abc" at position 1 has face bold
|
|
(should (eq (tp-at 1 'face) 'bold))
|
|
;; Check that "123" at position 5 has face bold
|
|
(should (eq (tp-at 5 'face) 'bold))
|
|
;; Check that "456" at position 13 has face bold
|
|
(should (eq (tp-at 13 'face) 'bold))
|
|
;; Check that "def" does NOT have face bold
|
|
(should (null (tp-at 9 'face))))))
|
|
|
|
(ert-deftest tp-test-regexp-set-multiple-patterns-on-string ()
|
|
"Test tp-regexp-set with multiple patterns on string."
|
|
(let* ((str (copy-sequence "abc 123 def 456"))
|
|
(result (tp-regexp-set '("[0-9]+" "abc") '(face italic) str)))
|
|
(should (stringp result))
|
|
;; Check that "abc" at position 0 has face italic
|
|
(should (eq (get-text-property 0 'face result) 'italic))
|
|
;; Check that "123" at position 4 has face italic
|
|
(should (eq (get-text-property 4 'face result) 'italic))
|
|
;; Check that "456" at position 12 has face italic
|
|
(should (eq (get-text-property 12 'face result) 'italic))
|
|
;; Check that "def" does NOT have face italic
|
|
(should (null (get-text-property 8 'face result)))))
|
|
|
|
(ert-deftest tp-test-get-range-multiple-intervals ()
|
|
"Test tp-get returns all property intervals in a range."
|
|
(let ((str (copy-sequence "Hello World Hello")))
|
|
(tp-set 0 5 '(face bold) str)
|
|
(tp-set 12 17 '(face italic) str)
|
|
(let ((intervals (tp-get 0 17 'face str)))
|
|
(should (= (length intervals) 2))
|
|
(should (equal (car intervals) '(0 5 bold)))
|
|
(should (equal (cadr intervals) '(12 17 italic))))))
|
|
|
|
;;; ============================================================
|
|
;;; New API Tests - Issue 1: tp-add face prepending
|
|
;;; ============================================================
|
|
|
|
(ert-deftest tp-test-add-face-prepend-symbol ()
|
|
"Test tp-add prepends face symbol to existing face."
|
|
(let ((str (copy-sequence "Hello")))
|
|
(tp-set 0 5 '(face bold) str)
|
|
(tp-add 0 5 '(face shadow) str)
|
|
(let ((face (get-text-property 0 'face str)))
|
|
;; New face should be prepended, creating a list
|
|
(should (equal face '(shadow bold))))))
|
|
|
|
(ert-deftest tp-test-add-face-prepend-to-list ()
|
|
"Test tp-add prepends face to existing face list."
|
|
(let ((str (copy-sequence "Hello")))
|
|
(tp-set 0 5 '(face (bold italic)) str)
|
|
(tp-add 0 5 '(face shadow) str)
|
|
(let ((face (get-text-property 0 'face str)))
|
|
;; New face should be prepended
|
|
(should (equal face '(shadow bold italic))))))
|
|
|
|
(ert-deftest tp-test-add-face-plist-merge ()
|
|
"Test tp-add merges face plist with existing face."
|
|
(let ((str (copy-sequence "Hello")))
|
|
(tp-set 0 5 '(face (:foreground "red")) str)
|
|
(tp-add 0 5 '(face (:background "blue")) str)
|
|
(let ((face (get-text-property 0 'face str)))
|
|
(should (equal (plist-get face :foreground) "red"))
|
|
(should (equal (plist-get face :background) "blue")))))
|
|
|
|
(ert-deftest tp-test-add-face-symbol-no-dup ()
|
|
"Test tp-add doesn't duplicate faces."
|
|
(let ((str (copy-sequence "Hello")))
|
|
(tp-set 0 5 '(face bold) str)
|
|
(tp-add 0 5 '(face bold) str)
|
|
(let ((face (get-text-property 0 'face str)))
|
|
;; Should not duplicate
|
|
(should (eq face 'bold)))))
|
|
|
|
;;; ============================================================
|
|
;;; New API Tests - Issue 2: tp-remove for strings
|
|
;;; ============================================================
|
|
|
|
(ert-deftest tp-test-remove-entire-string-single-prop ()
|
|
"Test tp-remove removes single property from entire string."
|
|
(let* ((str (tp-set "Hello" 'face 'bold 'help-echo "test"))
|
|
(result (tp-remove str 'face)))
|
|
(should (null (get-text-property 0 'face result)))
|
|
(should (equal (get-text-property 0 'help-echo result) "test"))))
|
|
|
|
(ert-deftest tp-test-remove-entire-string-multiple-props ()
|
|
"Test tp-remove removes multiple properties from entire string."
|
|
(let* ((str (tp-set "Hello" 'face 'bold 'help-echo "test" 'mouse-face 'highlight))
|
|
(result (tp-remove str 'face 'help-echo)))
|
|
(should (null (get-text-property 0 'face result)))
|
|
(should (null (get-text-property 0 'help-echo result)))
|
|
(should (eq (get-text-property 0 'mouse-face result) 'highlight))))
|
|
|
|
(ert-deftest tp-test-remove-entire-string-sub-prop ()
|
|
"Test tp-remove removes sub-property from entire string."
|
|
(let* ((str (copy-sequence "Hello"))
|
|
(_ (put-text-property 0 5 'face '(:foreground "red" :underline t) str))
|
|
(result (tp-remove str 'face :underline)))
|
|
(let ((face (get-text-property 0 'face result)))
|
|
(should (equal (plist-get face :foreground) "red"))
|
|
(should (null (plist-get face :underline))))))
|
|
|
|
(ert-deftest tp-test-remove-entire-string-nested-sub-prop ()
|
|
"Test tp-remove removes nested sub-properties from entire string."
|
|
(let* ((str (copy-sequence "Hello"))
|
|
(_ (put-text-property 0 5 'face '(:foreground "red" :underline (:style wave :color "blue")) str))
|
|
(result (tp-remove str 'face :underline '(:style))))
|
|
(let* ((face (get-text-property 0 'face result))
|
|
(underline (plist-get face :underline)))
|
|
(should (equal (plist-get face :foreground) "red"))
|
|
(should (equal (plist-get underline :color) "blue"))
|
|
(should (null (plist-get underline :style))))))
|
|
|
|
(ert-deftest tp-test-remove-entire-string-single-nested-key ()
|
|
"Test tp-remove removes a single nested key from a sub-property.
|
|
This tests the fix for the bug where (tp-remove str 'face :underline :position)
|
|
was removing the entire :underline instead of just :position."
|
|
(let* ((str (tp-set "happy hacking emacs"
|
|
'face '(:foreground "red" :underline (:position t :color "green"))
|
|
'line-prefix ">> " 'other "other"))
|
|
(result (tp-remove str 'face :underline :position)))
|
|
(let* ((face (get-text-property 0 'face result))
|
|
(underline (plist-get face :underline)))
|
|
;; :foreground should be preserved
|
|
(should (equal (plist-get face :foreground) "red"))
|
|
;; :underline should still exist but without :position
|
|
(should underline)
|
|
(should (equal (plist-get underline :color) "green"))
|
|
(should (null (plist-get underline :position)))
|
|
;; Other properties should be preserved
|
|
(should (equal (get-text-property 0 'line-prefix result) ">> "))
|
|
(should (equal (get-text-property 0 'other result) "other")))))
|
|
|
|
;;; ============================================================
|
|
;;; New API Tests - Issue 3 & 4: tp-get for strings and new API
|
|
;;; ============================================================
|
|
|
|
(ert-deftest tp-test-get-entire-string-all-props ()
|
|
"Test tp-get returns all property intervals from entire string."
|
|
(let ((str (tp-set "Hello" 'face 'bold 'help-echo "test")))
|
|
(let ((intervals (tp-get str)))
|
|
(should (= (length intervals) 1))
|
|
(let ((props (caddr (car intervals))))
|
|
(should (eq (plist-get props 'face) 'bold))
|
|
(should (equal (plist-get props 'help-echo) "test"))))))
|
|
|
|
(ert-deftest tp-test-get-entire-string-single-prop ()
|
|
"Test tp-get returns single property intervals from entire string."
|
|
(let ((str (tp-set "Hello" 'face 'bold 'help-echo "test")))
|
|
(let ((face-intervals (tp-get str 'face))
|
|
(help-intervals (tp-get str 'help-echo)))
|
|
(should (= (length face-intervals) 1))
|
|
(should (eq (caddr (car face-intervals)) 'bold))
|
|
(should (= (length help-intervals) 1))
|
|
(should (equal (caddr (car help-intervals)) "test")))))
|
|
|
|
(ert-deftest tp-test-get-entire-string-nested-prop ()
|
|
"Test tp-get returns nested property intervals from entire string."
|
|
(let ((str (copy-sequence "Hello")))
|
|
(put-text-property 0 5 'face '(:foreground "red" :box (:color "blue" :line-width 2)) str)
|
|
(let ((fg-intervals (tp-get str 'face :foreground))
|
|
(box-color-intervals (tp-get str 'face :box :color))
|
|
(box-width-intervals (tp-get str 'face :box :line-width)))
|
|
(should (= (length fg-intervals) 1))
|
|
(should (equal (caddr (car fg-intervals)) "red"))
|
|
(should (= (length box-color-intervals) 1))
|
|
(should (equal (caddr (car box-color-intervals)) "blue"))
|
|
(should (= (length box-width-intervals) 1))
|
|
(should (equal (caddr (car box-width-intervals)) 2)))))
|
|
|
|
(ert-deftest tp-test-get-range-with-list-prop-path ()
|
|
"Test tp-get with property path as list.
|
|
Returns list of (START END VALUE) intervals."
|
|
(tp-test-with-temp-buffer
|
|
(insert "Hello World")
|
|
(put-text-property 1 6 'face '(:foreground "red" :underline (:style wave)) nil)
|
|
;; Get with list path - returns intervals
|
|
(should (equal (tp-get 1 6 '(face)) '((1 6 (:foreground "red" :underline (:style wave))))))
|
|
(should (equal (tp-get 1 6 '(face :foreground)) '((1 6 "red"))))
|
|
(should (equal (tp-get 1 6 '(face :underline :style)) '((1 6 wave))))))
|
|
|
|
(ert-deftest tp-test-get-range-with-list-prop-path-on-string ()
|
|
"Test tp-get with property path as list on string.
|
|
Returns list of (START END VALUE) intervals."
|
|
(let ((str (copy-sequence "Hello World")))
|
|
(put-text-property 0 5 'face '(:foreground "red" :underline (:style wave)) str)
|
|
;; Get with list path and object - returns intervals
|
|
(should (equal (tp-get 0 5 '(face) str) '((0 5 (:foreground "red" :underline (:style wave))))))
|
|
(should (equal (tp-get 0 5 '(face :foreground) str) '((0 5 "red"))))
|
|
(should (equal (tp-get 0 5 '(face :underline :style) str) '((0 5 wave))))))
|
|
|
|
(ert-deftest tp-test-get-entire-string-with-list-prop-path ()
|
|
"Test tp-get with property path as list on entire string.
|
|
Returns list of (START END VALUE) intervals."
|
|
(let ((str (copy-sequence "Hello World Hello")))
|
|
(put-text-property 0 5 'face '(:foreground "red") str)
|
|
(put-text-property 12 17 'face '(:foreground "blue") str)
|
|
;; Get with list path for entire string
|
|
(let ((intervals (tp-get str '(face :foreground))))
|
|
(should (= (length intervals) 2))
|
|
(should (equal (car intervals) '(0 5 "red")))
|
|
(should (equal (cadr intervals) '(12 17 "blue"))))))
|
|
|
|
(ert-deftest tp-test-get-entire-string-multiple-intervals ()
|
|
"Test tp-get returns multiple intervals from entire string."
|
|
(let ((str (copy-sequence "Hello World Hello")))
|
|
(tp-set 0 5 '(face bold) str)
|
|
(tp-set 12 17 '(face italic) str)
|
|
(let ((intervals (tp-get str 'face)))
|
|
(should (= (length intervals) 2))
|
|
(should (equal (car intervals) '(0 5 bold)))
|
|
(should (equal (cadr intervals) '(12 17 italic))))))
|
|
|
|
(ert-deftest tp-test-get-deeply-nested-property ()
|
|
"Test tp-get with deeply nested property path."
|
|
(let ((str (copy-sequence "Hello World")))
|
|
(put-text-property 0 5 'face '(:foreground "red" :underline (:color "green" :style wave)) str)
|
|
(put-text-property 6 11 'face '(:foreground "blue" :underline (:color "yellow" :style line)) str)
|
|
;; Test deeply nested single key from entire string
|
|
(let ((intervals (tp-get str 'face :underline :color)))
|
|
(should (= (length intervals) 2))
|
|
(should (equal (caddr (car intervals)) "green"))
|
|
(should (equal (caddr (cadr intervals)) "yellow")))
|
|
;; Test range with deeply nested key
|
|
(let ((intervals (tp-get 0 7 '(face :underline :color) str)))
|
|
(should (= (length intervals) 2))
|
|
(should (equal (caddr (car intervals)) "green")))))
|
|
|
|
(ert-deftest tp-test-get-multiple-nested-keys ()
|
|
"Test tp-get with multiple keys from nested property."
|
|
(let ((str (copy-sequence "Hello World")))
|
|
(put-text-property 0 5 'face '(:foreground "red" :underline (:color "green" :style wave)) str)
|
|
(put-text-property 6 11 'face '(:foreground "blue" :underline (:color "yellow" :style line)) str)
|
|
;; Test extracting multiple keys from entire string
|
|
(let ((intervals (tp-get str 'face :underline '(:color :style))))
|
|
(should (= (length intervals) 2))
|
|
(let ((val1 (caddr (car intervals)))
|
|
(val2 (caddr (cadr intervals))))
|
|
(should (equal (plist-get val1 :color) "green"))
|
|
(should (eq (plist-get val1 :style) 'wave))
|
|
(should (equal (plist-get val2 :color) "yellow"))
|
|
(should (eq (plist-get val2 :style) 'line))))
|
|
;; Test range with multiple keys
|
|
(let ((intervals (tp-get 0 7 '(face :underline (:color :style)) str)))
|
|
(should (= (length intervals) 2)))))
|
|
|
|
;;; ============================================================
|
|
;;; tp-add-to-layers and tp-add-to-all-layers Tests
|
|
;;; ============================================================
|
|
|
|
(ert-deftest tp-test-add-to-layers-buffer ()
|
|
"Test tp-add-to-layers adds properties to specified layers in buffer."
|
|
(tp-test-with-temp-buffer
|
|
(insert "Hello")
|
|
(define-tp layer1 () '(face bold))
|
|
(define-tp layer2 () '(face italic))
|
|
(define-tp layer3 () '(face underline))
|
|
(tp-push-layer 1 6 'layer1)
|
|
(tp-push-layer 1 6 'layer2)
|
|
(tp-push-layer 1 6 'layer3)
|
|
;; Add help-echo to layer1 and layer3
|
|
(tp-add-to-layers '(layer1 layer3) 1 6 '(help-echo "test"))
|
|
;; layer3 is on top, should have help-echo
|
|
(should (equal (tp-at 1 'help-echo) "test"))
|
|
;; Check layer1 also got help-echo
|
|
(let ((layer1-props (car (tp-region-layer-props 1 6 'layer1))))
|
|
(should (equal (plist-get (caddr layer1-props) 'help-echo) "test")))
|
|
;; layer2 should NOT have help-echo
|
|
(let ((layer2-props (car (tp-region-layer-props 1 6 'layer2))))
|
|
(should (null (plist-get (caddr layer2-props) 'help-echo))))))
|
|
|
|
(ert-deftest tp-test-add-to-layers-by-index ()
|
|
"Test tp-add-to-layers with layer indices."
|
|
(tp-test-with-temp-buffer
|
|
(insert "Hello")
|
|
(define-tp layer1 () '(face bold))
|
|
(define-tp layer2 () '(face italic))
|
|
(define-tp layer3 () '(face underline))
|
|
(tp-push-layer 1 6 'layer1)
|
|
(tp-push-layer 1 6 'layer2)
|
|
(tp-push-layer 1 6 'layer3)
|
|
;; Stack is: layer3 (0), layer2 (1), layer1 (2)
|
|
;; Add help-echo to indices 0 and 2 (layer3 and layer1)
|
|
(tp-add-to-layers '(0 2) 1 6 '(help-echo "indexed"))
|
|
;; layer3 (top) should have help-echo
|
|
(should (equal (tp-at 1 'help-echo) "indexed"))
|
|
;; Check layer1 also got help-echo
|
|
(let ((layer1-props (car (tp-region-layer-props 1 6 'layer1))))
|
|
(should (equal (plist-get (caddr layer1-props) 'help-echo) "indexed")))
|
|
;; layer2 (index 1) should NOT have help-echo
|
|
(let ((layer2-props (car (tp-region-layer-props 1 6 'layer2))))
|
|
(should (null (plist-get (caddr layer2-props) 'help-echo))))))
|
|
|
|
(ert-deftest tp-test-add-to-layers-string ()
|
|
"Test tp-add-to-layers works on entire string."
|
|
(let ((str (copy-sequence "Hello")))
|
|
(setq tp-layer-alist nil)
|
|
(setq tp-layer-groups nil)
|
|
(define-tp layer1 () '(face bold))
|
|
(define-tp layer2 () '(face italic))
|
|
(tp-push-layer str 'layer1)
|
|
(tp-push-layer str 'layer2)
|
|
;; Add help-echo to layer1
|
|
(tp-add-to-layers '(layer1) str 'help-echo "test")
|
|
;; Check layer1 got help-echo
|
|
(let ((layer1-props (car (tp-region-layer-props 0 5 'layer1 str))))
|
|
(should (equal (plist-get (caddr layer1-props) 'help-echo) "test")))
|
|
;; layer2 (top) should NOT have help-echo
|
|
(should (null (tp-at 0 'help-echo str)))))
|
|
|
|
(ert-deftest tp-test-add-to-layers-deep-merge ()
|
|
"Test tp-add-to-layers deeply merges properties."
|
|
(tp-test-with-temp-buffer
|
|
(insert "Hello")
|
|
(define-tp layer1 () '(face (:foreground "red")))
|
|
(tp-push-layer 1 6 'layer1)
|
|
;; Add background to layer1 - should merge with existing face
|
|
(tp-add-to-layers '(layer1) 1 6 '(face (:background "blue")))
|
|
(let ((face (tp-at 1 'face)))
|
|
(should (equal (plist-get face :foreground) "red"))
|
|
(should (equal (plist-get face :background) "blue")))))
|
|
|
|
(ert-deftest tp-test-add-to-all-layers-buffer ()
|
|
"Test tp-add-to-all-layers adds properties to all layers in buffer."
|
|
(tp-test-with-temp-buffer
|
|
(insert "Hello")
|
|
(define-tp layer1 () '(face bold))
|
|
(define-tp layer2 () '(face italic))
|
|
(define-tp layer3 () '(face underline))
|
|
(tp-push-layer 1 6 'layer1)
|
|
(tp-push-layer 1 6 'layer2)
|
|
(tp-push-layer 1 6 'layer3)
|
|
;; Add help-echo to all layers
|
|
(tp-add-to-all-layers 1 6 '(help-echo "all"))
|
|
;; layer3 (top) should have help-echo
|
|
(should (equal (tp-at 1 'help-echo) "all"))
|
|
;; Check all layers got help-echo
|
|
(let ((layer1-props (car (tp-region-layer-props 1 6 'layer1)))
|
|
(layer2-props (car (tp-region-layer-props 1 6 'layer2)))
|
|
(layer3-props (car (tp-region-layer-props 1 6 'layer3))))
|
|
(should (equal (plist-get (caddr layer1-props) 'help-echo) "all"))
|
|
(should (equal (plist-get (caddr layer2-props) 'help-echo) "all"))
|
|
(should (equal (plist-get (caddr layer3-props) 'help-echo) "all")))))
|
|
|
|
(ert-deftest tp-test-add-to-all-layers-string ()
|
|
"Test tp-add-to-all-layers works on entire string."
|
|
(let ((str (copy-sequence "Hello")))
|
|
(setq tp-layer-alist nil)
|
|
(setq tp-layer-groups nil)
|
|
(define-tp layer1 () '(face bold))
|
|
(define-tp layer2 () '(face italic))
|
|
(tp-push-layer str 'layer1)
|
|
(tp-push-layer str 'layer2)
|
|
;; Add help-echo to all layers
|
|
(tp-add-to-all-layers str 'help-echo "all")
|
|
;; Check all layers got help-echo
|
|
(let ((layer1-props (car (tp-region-layer-props 0 5 'layer1 str)))
|
|
(layer2-props (car (tp-region-layer-props 0 5 'layer2 str))))
|
|
(should (equal (plist-get (caddr layer1-props) 'help-echo) "all"))
|
|
(should (equal (plist-get (caddr layer2-props) 'help-echo) "all")))))
|
|
|
|
(ert-deftest tp-test-add-to-all-layers-deep-merge ()
|
|
"Test tp-add-to-all-layers deeply merges properties."
|
|
(tp-test-with-temp-buffer
|
|
(insert "Hello")
|
|
(define-tp layer1 () '(face (:foreground "red")))
|
|
(define-tp layer2 () '(face (:foreground "blue")))
|
|
(tp-push-layer 1 6 'layer1)
|
|
(tp-push-layer 1 6 'layer2)
|
|
;; Add background to all layers
|
|
(tp-add-to-all-layers 1 6 '(face (:background "green")))
|
|
;; Top layer (layer2) should have merged face
|
|
(let ((face (tp-at 1 'face)))
|
|
(should (equal (plist-get face :foreground) "blue"))
|
|
(should (equal (plist-get face :background) "green")))
|
|
;; layer1 should also have merged face
|
|
(let* ((layer1-props (car (tp-region-layer-props 1 6 'layer1)))
|
|
(face (plist-get (caddr layer1-props) 'face)))
|
|
(should (equal (plist-get face :foreground) "red"))
|
|
(should (equal (plist-get face :background) "green")))))
|
|
|
|
(ert-deftest tp-test-add-to-layers-negative-index ()
|
|
"Test tp-add-to-layers with negative index (-1 means bottom)."
|
|
(tp-test-with-temp-buffer
|
|
(insert "Hello")
|
|
(define-tp layer1 () '(face bold))
|
|
(define-tp layer2 () '(face italic))
|
|
(tp-push-layer 1 6 'layer1)
|
|
(tp-push-layer 1 6 'layer2)
|
|
;; Stack is: layer2 (0), layer1 (1)
|
|
;; Add help-echo to index -1 (bottom = layer1)
|
|
(tp-add-to-layers '(-1) 1 6 '(help-echo "bottom"))
|
|
;; layer2 (top) should NOT have help-echo
|
|
(should (null (tp-at 1 'help-echo)))
|
|
;; layer1 (bottom) should have help-echo
|
|
(let ((layer1-props (car (tp-region-layer-props 1 6 'layer1))))
|
|
(should (equal (plist-get (caddr layer1-props) 'help-echo) "bottom")))))
|
|
|
|
(ert-deftest tp-test-add-to-layers-returns-string ()
|
|
"Test tp-add-to-layers returns the modified string."
|
|
(let ((str (copy-sequence "Hello")))
|
|
(setq tp-layer-alist nil)
|
|
(setq tp-layer-groups nil)
|
|
(define-tp layer1 () '(face bold))
|
|
(tp-push-layer str 'layer1)
|
|
(let ((result (tp-add-to-layers '(layer1) str 'help-echo "test")))
|
|
(should (stringp result))
|
|
(should (eq result str)))))
|
|
|
|
(ert-deftest tp-test-add-to-all-layers-returns-string ()
|
|
"Test tp-add-to-all-layers returns the modified string."
|
|
(let ((str (copy-sequence "Hello")))
|
|
(setq tp-layer-alist nil)
|
|
(setq tp-layer-groups nil)
|
|
(define-tp layer1 () '(face bold))
|
|
(tp-push-layer str 'layer1)
|
|
(let ((result (tp-add-to-all-layers str 'help-echo "test")))
|
|
(should (stringp result))
|
|
(should (eq result str)))))
|
|
|
|
;;; ============================================================
|
|
;;; Reactive Text Properties Tests
|
|
;;; ============================================================
|
|
|
|
(ert-deftest tp-test-reactive-symbol-p ()
|
|
"Test tp--reactive-symbol-p detects $-prefixed symbols."
|
|
(should (tp--reactive-symbol-p '$foo))
|
|
(should (tp--reactive-symbol-p '$my-color))
|
|
(should-not (tp--reactive-symbol-p 'foo))
|
|
(should-not (tp--reactive-symbol-p "string"))
|
|
(should-not (tp--reactive-symbol-p 42)))
|
|
|
|
(ert-deftest tp-test-reactive-var-symbol ()
|
|
"Test tp--reactive-var-symbol converts $foo to foo."
|
|
(should (eq (tp--reactive-var-symbol '$foo) 'foo))
|
|
(should (eq (tp--reactive-var-symbol '$my-color) 'my-color))
|
|
(should (null (tp--reactive-var-symbol 'foo)))
|
|
(should (null (tp--reactive-var-symbol "string"))))
|
|
|
|
(ert-deftest tp-test-collect-reactive-symbols ()
|
|
"Test tp--collect-reactive-symbols finds all $-prefixed symbols."
|
|
(should (equal (tp--collect-reactive-symbols '$foo) '($foo)))
|
|
(should (equal (tp--collect-reactive-symbols '(face (:foreground $color)))
|
|
'($color)))
|
|
(should (equal (tp--collect-reactive-symbols '(face (:foreground $color :background $bg)))
|
|
'($color $bg)))
|
|
(should (null (tp--collect-reactive-symbols '(face bold)))))
|
|
|
|
(ert-deftest tp-test-extract-reactive-props ()
|
|
"Test tp--extract-reactive-props extracts only properties using a reactive var."
|
|
;; Single reactive property
|
|
(should (equal (tp--extract-reactive-props '(help-echo "test" face (:foreground $color)) '$color)
|
|
'(face (:foreground $color))))
|
|
;; Multiple properties, only one uses the variable - should extract only reactive sub-props
|
|
(should (equal (tp--extract-reactive-props '(help-echo "test" face (:foreground $color :background "green")) '$color)
|
|
'(face (:foreground $color))))
|
|
;; Nested plist with reactive variable - should extract only reactive nested sub-props
|
|
(should (equal (tp--extract-reactive-props
|
|
'(face (:foreground $color1 :underline (:style wave :color $color2 :position t)))
|
|
'$color2)
|
|
'(face (:underline (:color $color2)))))
|
|
;; No properties use the variable
|
|
(should (null (tp--extract-reactive-props '(help-echo "test" face bold) '$color))))
|
|
|
|
(ert-deftest tp-test-resolve-reactive-symbols ()
|
|
"Test tp--resolve-reactive-symbols replaces $foo with variable values."
|
|
;; Use defvar to create dynamically-bound variables
|
|
(defvar tp-test-my-color "red" "Test color variable.")
|
|
(defvar tp-test-my-bg "blue" "Test background variable.")
|
|
(unwind-protect
|
|
(progn
|
|
(should (equal (tp--resolve-reactive-symbols '$tp-test-my-color) "red"))
|
|
(should (equal (tp--resolve-reactive-symbols '(face (:foreground $tp-test-my-color)))
|
|
'(face (:foreground "red"))))
|
|
(should (equal (tp--resolve-reactive-symbols '(face (:foreground $tp-test-my-color :background $tp-test-my-bg)))
|
|
'(face (:foreground "red" :background "blue")))))
|
|
;; Cleanup
|
|
(makunbound 'tp-test-my-color)
|
|
(makunbound 'tp-test-my-bg)))
|
|
|
|
(ert-deftest tp-test-define-layer-with-reactive ()
|
|
"Test define-tp with reactive variables."
|
|
(tp-test-with-temp-buffer
|
|
(defvar tp-test-var-color "red" "Test color variable.")
|
|
(unwind-protect
|
|
(progn
|
|
(define-tp test-reactive-layer () '(face (:foreground $tp-test-var-color)))
|
|
;; Check the layer is defined with resolved value
|
|
(let ((props (cdr (assoc 'test-reactive-layer tp-layer-alist))))
|
|
(should (equal (plist-get (plist-get props 'face) :foreground) "red")))
|
|
;; Check the dependency is registered with only reactive props
|
|
(should (assoc 'tp-test-var-color tp-reactive-deps))
|
|
;; Check the stored reactive props only contain the face property
|
|
(let* ((deps (cdr (assoc 'tp-test-var-color tp-reactive-deps)))
|
|
(layer-dep (assoc 'test-reactive-layer deps)))
|
|
(should layer-dep)
|
|
;; The stored props should be just the reactive portion
|
|
(should (plist-get (cdr layer-dep) 'face))))
|
|
;; Cleanup
|
|
(makunbound 'tp-test-var-color))))
|
|
|
|
(ert-deftest tp-test-reactive-update-on-variable-change ()
|
|
"Test that changing a reactive variable updates the layer."
|
|
(tp-test-with-temp-buffer
|
|
(defvar tp-test-reactive-color nil "Test variable for reactive properties.")
|
|
(setq tp-test-reactive-color "red")
|
|
(unwind-protect
|
|
(progn
|
|
(define-tp test-reactive-update () '(face (:foreground $tp-test-reactive-color)))
|
|
;; Verify initial value
|
|
(let ((props (cdr (assoc 'test-reactive-update tp-layer-alist))))
|
|
(should (equal (plist-get (plist-get props 'face) :foreground) "red")))
|
|
;; Change the variable
|
|
(setq tp-test-reactive-color "blue")
|
|
;; Verify the layer definition is updated
|
|
(let ((props (cdr (assoc 'test-reactive-update tp-layer-alist))))
|
|
(should (equal (plist-get (plist-get props 'face) :foreground) "blue"))))
|
|
;; Cleanup
|
|
(makunbound 'tp-test-reactive-color))))
|
|
|
|
(ert-deftest tp-test-reactive-update-text-regions ()
|
|
"Test that changing a reactive variable updates applied text regions."
|
|
(tp-test-with-temp-buffer
|
|
(defvar tp-test-region-color nil "Test variable for reactive regions.")
|
|
(setq tp-test-region-color "red")
|
|
(unwind-protect
|
|
(progn
|
|
(define-tp test-reactive-region () '(face (:foreground $tp-test-region-color)))
|
|
(insert "Hello World")
|
|
;; Apply the layer to text
|
|
(tp-push-layer 1 6 'test-reactive-region)
|
|
;; Verify initial properties
|
|
(should (equal (plist-get (tp-at 1 'face) :foreground) "red"))
|
|
;; Change the variable
|
|
(setq tp-test-region-color "green")
|
|
;; Verify the text is updated
|
|
(should (equal (plist-get (tp-at 1 'face) :foreground) "green")))
|
|
;; Cleanup
|
|
(makunbound 'tp-test-region-color))))
|
|
|
|
(ert-deftest tp-test-reactive-reset ()
|
|
"Test tp-reactive-reset clears all reactive dependencies."
|
|
(tp-test-with-temp-buffer
|
|
(defvar tp-test-reset-color nil "Test variable for reactive reset.")
|
|
(setq tp-test-reset-color "red")
|
|
(unwind-protect
|
|
(progn
|
|
(define-tp test-reactive-reset () '(face (:foreground $tp-test-reset-color)))
|
|
(should tp-reactive-deps)
|
|
(tp-reactive-reset)
|
|
(should-not tp-reactive-deps))
|
|
;; Cleanup
|
|
(makunbound 'tp-test-reset-color))))
|
|
|
|
(ert-deftest tp-test-layer-reset-clears-reactive ()
|
|
"Test tp-layer-reset also clears reactive dependencies."
|
|
(tp-test-with-temp-buffer
|
|
(defvar tp-test-reset2-color nil "Test variable for layer reset.")
|
|
(setq tp-test-reset2-color "red")
|
|
(unwind-protect
|
|
(progn
|
|
(define-tp test-reactive-reset2 () '(face (:foreground $tp-test-reset2-color)))
|
|
(should tp-reactive-deps)
|
|
(tp-layer-reset)
|
|
(should-not tp-reactive-deps))
|
|
;; Cleanup
|
|
(makunbound 'tp-test-reset2-color))))
|
|
|
|
(ert-deftest tp-test-define-layer-group-with-reactive ()
|
|
"Test define-tps with reactive variables."
|
|
(tp-test-with-temp-buffer
|
|
(defvar tp-test-group-color nil "Test variable for layer group.")
|
|
(setq tp-test-group-color "red")
|
|
(unwind-protect
|
|
(progn
|
|
(define-tps test-reactive-group ()
|
|
'("first" :props (face (:foreground $tp-test-group-color)))
|
|
'("second" :props (face (:foreground "blue"))))
|
|
;; Check the reactive layer is defined with resolved value
|
|
(let ((props (cdr (assoc 'test-reactive-group-first tp-layer-alist))))
|
|
(should (equal (plist-get (plist-get props 'face) :foreground) "red")))
|
|
;; Check the non-reactive layer is defined
|
|
(let ((props (cdr (assoc 'test-reactive-group-second tp-layer-alist))))
|
|
(should (equal (plist-get (plist-get props 'face) :foreground) "blue")))
|
|
;; Check the reactive layer is registered in tp-reactive-deps
|
|
(should (assoc 'tp-test-group-color tp-reactive-deps))
|
|
;; The reactive layer should be in the dependencies
|
|
(let* ((deps (cdr (assoc 'tp-test-group-color tp-reactive-deps)))
|
|
(layer-dep (assoc 'test-reactive-group-first deps)))
|
|
(should layer-dep)))
|
|
;; Cleanup
|
|
(makunbound 'tp-test-group-color))))
|
|
|
|
(ert-deftest tp-test-undefine-layer-clears-reactive ()
|
|
"Test tp-undefine-layer clears reactive dependencies for that layer."
|
|
(tp-test-with-temp-buffer
|
|
(defvar tp-test-undef-color nil "Test variable for undefine.")
|
|
(setq tp-test-undef-color "red")
|
|
(unwind-protect
|
|
(progn
|
|
(define-tp test-undef-reactive () '(face (:foreground $tp-test-undef-color)))
|
|
;; Check the dependency is registered
|
|
(should (assoc 'tp-test-undef-color tp-reactive-deps))
|
|
(let* ((deps (cdr (assoc 'tp-test-undef-color tp-reactive-deps)))
|
|
(layer-dep (assoc 'test-undef-reactive deps)))
|
|
(should layer-dep))
|
|
(tp-undefine-layer 'test-undef-reactive)
|
|
;; Dependency should be cleaned up if no other layers use it
|
|
(should-not (cdr (assoc 'tp-test-undef-color tp-reactive-deps))))
|
|
;; Cleanup
|
|
(makunbound 'tp-test-undef-color))))
|
|
|
|
;;; ============================================================
|
|
;;; Layer Name in Property-Setting APIs Tests
|
|
;;; ============================================================
|
|
|
|
(ert-deftest tp-test-set-with-layer-name ()
|
|
"Test tp-set accepts a layer name defined by define-tp.
|
|
When using tp-set (direct property setting), tp-name is NOT added."
|
|
(tp-test-with-temp-buffer
|
|
(insert "Hello World")
|
|
(define-tp my-style () '(face bold help-echo "tip"))
|
|
;; Use layer name instead of plist
|
|
(tp-set 1 6 'my-style)
|
|
(should (eq (tp-at 1 'face) 'bold))
|
|
(should (equal (tp-at 1 'help-echo) "tip"))
|
|
;; tp-name should NOT be set for direct property setting
|
|
(should-not (tp-at 1 'tp-name))))
|
|
|
|
(ert-deftest tp-test-set-with-layer-name-on-string ()
|
|
"Test tp-set accepts a layer name on string.
|
|
When using tp-set (direct property setting), tp-name is NOT added."
|
|
(let ((str (copy-sequence "Hello World")))
|
|
(setq tp-layer-alist nil)
|
|
(setq tp-layer-groups nil)
|
|
(define-tp my-style () '(face italic))
|
|
(tp-set 0 5 'my-style str)
|
|
(should (eq (get-text-property 0 'face str) 'italic))
|
|
;; tp-name should NOT be set for direct property setting
|
|
(should-not (get-text-property 0 'tp-name str))))
|
|
|
|
(ert-deftest tp-test-set-entire-string-with-layer-name ()
|
|
"Test tp-set with layer name on entire string (string form).
|
|
This tests the fix for the bug where (tp-set str 'layer-name) would
|
|
incorrectly generate an anonymous tp-name instead of using the layer name."
|
|
(tp-test-with-temp-buffer
|
|
;; Define a layer with reactive variables
|
|
(define-tp my-entire-string-layer () :props '(face (:background $my-entire-string-color))
|
|
:data '((my-entire-string-color . "blue")))
|
|
(let ((str (tp-set " " 'my-entire-string-layer)))
|
|
;; tp-name should be the defined layer name, not an anonymous tp-anon-X
|
|
(should (eq (get-text-property 0 'tp-name str) 'my-entire-string-layer))
|
|
;; face should be correctly set
|
|
(should (equal (plist-get (get-text-property 0 'face str) :background) "blue")))))
|
|
|
|
(ert-deftest tp-test-reset-with-layer-name ()
|
|
"Test tp-reset accepts a layer name defined by define-tp.
|
|
When using tp-reset (direct property setting), tp-name is NOT added."
|
|
(tp-test-with-temp-buffer
|
|
(insert "Hello World")
|
|
(tp-set 1 6 '(mouse-face highlight))
|
|
(define-tp my-style () '(face underline))
|
|
;; Use layer name - should completely replace
|
|
(tp-reset 1 6 'my-style)
|
|
(should (eq (tp-at 1 'face) 'underline))
|
|
(should (null (tp-at 1 'mouse-face)))
|
|
;; tp-name should NOT be set for direct property setting
|
|
(should-not (tp-at 1 'tp-name))))
|
|
|
|
(ert-deftest tp-test-add-with-layer-name ()
|
|
"Test tp-add accepts a layer name defined by define-tp.
|
|
When using tp-add (direct property setting), tp-name is NOT added."
|
|
(tp-test-with-temp-buffer
|
|
(insert "Hello World")
|
|
(tp-set 1 6 '(help-echo "existing"))
|
|
(define-tp my-style () '(face bold))
|
|
;; Use layer name - should preserve existing properties
|
|
(tp-add 1 6 'my-style)
|
|
(should (eq (tp-at 1 'face) 'bold))
|
|
(should (equal (tp-at 1 'help-echo) "existing"))
|
|
;; tp-name should NOT be set for direct property setting
|
|
(should-not (tp-at 1 'tp-name))))
|
|
|
|
(ert-deftest tp-test-match-set-with-layer-name ()
|
|
"Test tp-match-set accepts a layer name.
|
|
When using tp-match-set (direct property setting), tp-name is NOT added."
|
|
(tp-test-with-temp-buffer
|
|
(insert "Hello World Hello")
|
|
(define-tp match-style () '(face bold help-echo "matched"))
|
|
(tp-match-set "Hello" 'match-style)
|
|
(should (eq (tp-at 1 'face) 'bold))
|
|
(should (equal (tp-at 1 'help-echo) "matched"))
|
|
(should (eq (tp-at 13 'face) 'bold))
|
|
;; tp-name should NOT be set for direct property setting
|
|
(should-not (tp-at 1 'tp-name))))
|
|
|
|
(ert-deftest tp-test-match-set-with-layer-name-on-string ()
|
|
"Test tp-match-set accepts a layer name on string.
|
|
When using tp-match-set (direct property setting), tp-name is NOT added.
|
|
For strings, returns a NEW string (original is not modified)."
|
|
(let ((str (copy-sequence "Hello World Hello")))
|
|
(setq tp-layer-alist nil)
|
|
(setq tp-layer-groups nil)
|
|
(define-tp match-style () '(face italic))
|
|
(let ((result (tp-match-set "Hello" 'match-style str)))
|
|
;; Result should have the properties
|
|
(should (eq (get-text-property 0 'face result) 'italic))
|
|
(should (eq (get-text-property 12 'face result) 'italic))
|
|
;; tp-name should NOT be set for direct property setting
|
|
(should-not (get-text-property 0 'tp-name result))
|
|
;; Original should NOT be modified
|
|
(should (null (get-text-property 0 'face str))))))
|
|
|
|
(ert-deftest tp-test-match-reset-with-layer-name ()
|
|
"Test tp-match-reset accepts a layer name."
|
|
(tp-test-with-temp-buffer
|
|
(insert "Hello World Hello")
|
|
(tp-set 1 6 '(mouse-face highlight))
|
|
(define-tp match-style () '(face bold))
|
|
(tp-match-reset "Hello" 'match-style)
|
|
(should (eq (tp-at 1 'face) 'bold))
|
|
(should (null (tp-at 1 'mouse-face)))))
|
|
|
|
(ert-deftest tp-test-match-add-with-layer-name ()
|
|
"Test tp-match-add accepts a layer name."
|
|
(tp-test-with-temp-buffer
|
|
(insert "Hello World Hello")
|
|
(tp-set 1 6 '(help-echo "original"))
|
|
(define-tp match-style () '(face bold))
|
|
(tp-match-add "Hello" 'match-style)
|
|
(should (eq (tp-at 1 'face) 'bold))
|
|
(should (equal (tp-at 1 'help-echo) "original"))))
|
|
|
|
(ert-deftest tp-test-regexp-set-with-layer-name ()
|
|
"Test tp-regexp-set accepts a layer name."
|
|
(tp-test-with-temp-buffer
|
|
(insert "abc 123 def 456")
|
|
(define-tp number-style () '(face bold help-echo "number"))
|
|
(tp-regexp-set "[0-9]+" 'number-style)
|
|
(should (eq (tp-at 5 'face) 'bold))
|
|
(should (equal (tp-at 5 'help-echo) "number"))
|
|
(should (eq (tp-at 13 'face) 'bold))))
|
|
|
|
(ert-deftest tp-test-regexp-set-with-layer-name-on-string ()
|
|
"Test tp-regexp-set accepts a layer name on string.
|
|
For strings, returns a NEW string (original is not modified)."
|
|
(let ((str (copy-sequence "abc 123 def 456")))
|
|
(setq tp-layer-alist nil)
|
|
(setq tp-layer-groups nil)
|
|
(define-tp number-style () '(face italic))
|
|
(let ((result (tp-regexp-set "[0-9]+" 'number-style str)))
|
|
;; Result should have the properties
|
|
(should (eq (get-text-property 4 'face result) 'italic))
|
|
(should (eq (get-text-property 12 'face result) 'italic))
|
|
;; Original should NOT be modified
|
|
(should (null (get-text-property 4 'face str))))))
|
|
|
|
(ert-deftest tp-test-regexp-reset-with-layer-name ()
|
|
"Test tp-regexp-reset accepts a layer name."
|
|
(tp-test-with-temp-buffer
|
|
(insert "abc 123 def 456")
|
|
(tp-set 5 8 '(mouse-face highlight))
|
|
(define-tp number-style () '(face bold))
|
|
(tp-regexp-reset "[0-9]+" 'number-style)
|
|
(should (eq (tp-at 5 'face) 'bold))
|
|
(should (null (tp-at 5 'mouse-face)))))
|
|
|
|
(ert-deftest tp-test-regexp-add-with-layer-name ()
|
|
"Test tp-regexp-add accepts a layer name.
|
|
When using tp-regexp-add (direct property setting), tp-name is NOT added."
|
|
(tp-test-with-temp-buffer
|
|
(insert "abc 123 def 456")
|
|
(tp-set 5 8 '(help-echo "original"))
|
|
(define-tp number-style () '(face bold))
|
|
(tp-regexp-add "[0-9]+" 'number-style)
|
|
(should (eq (tp-at 5 'face) 'bold))
|
|
(should (equal (tp-at 5 'help-echo) "original"))
|
|
;; tp-name should NOT be set for direct property setting
|
|
(should-not (tp-at 5 'tp-name))))
|
|
|
|
(ert-deftest tp-test-set-with-group-name ()
|
|
"Test tp-set accepts a group name defined by define-tps.
|
|
When using tp-set with a group, layers are set with tp-name and tp-layers."
|
|
(tp-test-with-temp-buffer
|
|
(insert "Hello World")
|
|
(define-tps my-group ()
|
|
'("style" . (face bold help-echo "grouped")))
|
|
;; Use group name
|
|
(tp-set 1 6 'my-group)
|
|
(should (eq (tp-at 1 'face) 'bold))
|
|
(should (equal (tp-at 1 'help-echo) "grouped"))
|
|
;; tp-name should be set for layer groups
|
|
(should (tp-at 1 'tp-name))))
|
|
|
|
(ert-deftest tp-test-set-with-group-name-multiple-layers ()
|
|
"Test tp-set with group containing multiple layers.
|
|
When using tp-set with a group, all layers are set with tp-name and tp-layers."
|
|
(tp-test-with-temp-buffer
|
|
(insert "Hello World")
|
|
(define-tps my-group ()
|
|
'("first" . (face bold))
|
|
'("second" . (face italic)))
|
|
;; Use group name - all layers are applied with tp-layers structure
|
|
(tp-set 1 6 'my-group)
|
|
;; First layer's properties are applied at top
|
|
(should (eq (tp-at 1 'face) 'bold))
|
|
;; tp-name should be set for the top layer
|
|
(should (tp-at 1 'tp-name))
|
|
;; tp-layers should contain the rest of the layers
|
|
(should (tp-at 1 'tp-layers))))
|
|
|
|
(ert-deftest tp-test-match-set-with-group-name ()
|
|
"Test tp-match-set accepts a group name.
|
|
When using tp-match-set with a group, layers are set with tp-name."
|
|
(tp-test-with-temp-buffer
|
|
(insert "Hello World Hello")
|
|
(define-tps my-group ()
|
|
'("style" . (face italic)))
|
|
(tp-match-set "Hello" 'my-group)
|
|
(should (eq (tp-at 1 'face) 'italic))
|
|
(should (eq (tp-at 13 'face) 'italic))
|
|
;; tp-name should be set for layer groups
|
|
(should (tp-at 1 'tp-name))))
|
|
|
|
(ert-deftest tp-test-resolve-props-returns-nil-for-unknown ()
|
|
"Test tp--resolve-props returns nil for unknown layer name."
|
|
(tp-test-with-temp-buffer
|
|
(should (null (tp--resolve-props 'unknown-layer-name)))))
|
|
|
|
(ert-deftest tp-test-set-with-complex-layer ()
|
|
"Test tp-set with layer containing complex nested properties."
|
|
(tp-test-with-temp-buffer
|
|
(insert "Hello World")
|
|
(define-tp complex-layer ()
|
|
'(face (:foreground "red" :underline (:style wave))
|
|
help-echo "complex"))
|
|
(tp-set 1 6 'complex-layer)
|
|
(let ((face (tp-at 1 'face)))
|
|
(should (equal (plist-get face :foreground) "red"))
|
|
(should (equal (plist-get (plist-get face :underline) :style) 'wave)))
|
|
(should (equal (tp-at 1 'help-echo) "complex"))))
|
|
|
|
;;; ============================================================
|
|
;;; Anonymous Layer and Reactive Text Property Tests
|
|
;;; ============================================================
|
|
|
|
(ert-deftest tp-test-set-anonymous-layer-no-tp-name-for-non-reactive ()
|
|
"Test that tp-set with non-reactive plist does NOT get tp-name.
|
|
Per requirement 1: non-reactive properties should not have tp-name added,
|
|
preserving the native text property behavior."
|
|
(tp-test-with-temp-buffer
|
|
(insert "Hello World")
|
|
(tp-set 1 6 '(face bold))
|
|
;; Non-reactive anonymous layer should NOT have tp-name
|
|
(should-not (tp-at 1 'tp-name))
|
|
;; But the face property should still be set
|
|
(should (eq (tp-at 1 'face) 'bold))))
|
|
|
|
(ert-deftest tp-test-set-anonymous-reactive-layer ()
|
|
"Test that tp-set with anonymous reactive plist works."
|
|
(tp-test-with-temp-buffer
|
|
(defvar tp-test-anon-color nil "Test variable for anonymous reactive layer.")
|
|
(setq tp-test-anon-color "red")
|
|
(unwind-protect
|
|
(progn
|
|
(insert "Hello World")
|
|
;; Set with anonymous reactive plist
|
|
(tp-set 1 6 '(face (:foreground $tp-test-anon-color)))
|
|
;; Should have resolved the reactive variable
|
|
(let ((face (tp-at 1 'face)))
|
|
(should (equal (plist-get face :foreground) "red")))
|
|
;; Should have a generated tp-name
|
|
(should (tp-at 1 'tp-name))
|
|
;; The reactive variable should be registered in dependencies
|
|
(should (assoc 'tp-test-anon-color tp-reactive-deps))
|
|
;; Change the variable - should update the text
|
|
(setq tp-test-anon-color "blue")
|
|
(let ((face (tp-at 1 'face)))
|
|
(should (equal (plist-get face :foreground) "blue"))))
|
|
;; Cleanup
|
|
(makunbound 'tp-test-anon-color))))
|
|
|
|
(ert-deftest tp-test-set-anonymous-layer-preserves-existing-tp-name ()
|
|
"Test that tp-set with anonymous plist preserves existing tp-name property."
|
|
(tp-test-with-temp-buffer
|
|
(insert "Hello World")
|
|
;; First set with a layer name - this does NOT set tp-name
|
|
(define-tp my-existing-layer () '(face bold))
|
|
(tp-set 1 6 'my-existing-layer)
|
|
(should-not (tp-at 1 'tp-name)) ; no tp-name for direct setting
|
|
;; Now set with anonymous plist that has explicit tp-name
|
|
(tp-set 1 6 '(face italic tp-name my-custom-name))
|
|
;; Explicit tp-name in plist should be preserved
|
|
(should (eq (tp-at 1 'tp-name) 'my-custom-name))))
|
|
|
|
(ert-deftest tp-test-match-set-anonymous-reactive-layer ()
|
|
"Test that tp-match-set with anonymous reactive plist works."
|
|
(tp-test-with-temp-buffer
|
|
(defvar tp-test-match-color nil "Test variable for match reactive layer.")
|
|
(setq tp-test-match-color "green")
|
|
(unwind-protect
|
|
(progn
|
|
(insert "Hello World Hello")
|
|
;; Set with anonymous reactive plist
|
|
(tp-match-set "Hello" '(face (:foreground $tp-test-match-color)))
|
|
;; Should have resolved the reactive variable
|
|
(let ((face (tp-at 1 'face)))
|
|
(should (equal (plist-get face :foreground) "green")))
|
|
;; Should have a generated tp-name
|
|
(should (tp-at 1 'tp-name))
|
|
;; Change the variable - should update the text
|
|
(setq tp-test-match-color "yellow")
|
|
(let ((face (tp-at 1 'face)))
|
|
(should (equal (plist-get face :foreground) "yellow"))))
|
|
;; Cleanup
|
|
(makunbound 'tp-test-match-color))))
|
|
|
|
(ert-deftest tp-test-regexp-set-anonymous-reactive-layer ()
|
|
"Test that tp-regexp-set with anonymous reactive plist works."
|
|
(tp-test-with-temp-buffer
|
|
(defvar tp-test-regexp-color nil "Test variable for regexp reactive layer.")
|
|
(setq tp-test-regexp-color "purple")
|
|
(unwind-protect
|
|
(progn
|
|
(insert "abc 123 def 456")
|
|
;; Set with anonymous reactive plist
|
|
(tp-regexp-set "[0-9]+" '(face (:foreground $tp-test-regexp-color)))
|
|
;; Should have resolved the reactive variable
|
|
(let ((face (tp-at 5 'face)))
|
|
(should (equal (plist-get face :foreground) "purple")))
|
|
;; Should have a generated tp-name
|
|
(should (tp-at 5 'tp-name))
|
|
;; Change the variable - should update the text
|
|
(setq tp-test-regexp-color "orange")
|
|
(let ((face (tp-at 5 'face)))
|
|
(should (equal (plist-get face :foreground) "orange"))))
|
|
;; Cleanup
|
|
(makunbound 'tp-test-regexp-color))))
|
|
|
|
;;; ============================================================
|
|
;;; :watch, :data, and :compute Tests (Vue 3 style reactivity)
|
|
;;; ============================================================
|
|
|
|
(ert-deftest tp-test-define-layer-with-watch ()
|
|
"Test reactive layers with :watch for side effects."
|
|
(tp-test-with-temp-buffer
|
|
(defvar tp-test-watch-var nil "Test variable for watch.")
|
|
(defvar tp-test-watch-log nil "Log of watch callback invocations.")
|
|
(setq tp-test-watch-var "initial")
|
|
(setq tp-test-watch-log nil)
|
|
(unwind-protect
|
|
(progn
|
|
(define-tp test-watch-layer () :props '(face (:foreground $tp-test-watch-var))
|
|
:watch '((tp-test-watch-var
|
|
(lambda (new old layer)
|
|
(push (list new old layer) tp-test-watch-log)))))
|
|
;; Check the layer is defined with resolved value
|
|
(let ((props (cdr (assoc 'test-watch-layer tp-layer-alist))))
|
|
(should (equal (plist-get (plist-get props 'face) :foreground) "initial")))
|
|
;; Check the watcher is registered
|
|
(should (assoc 'test-watch-layer tp-layer-watchers))
|
|
;; Change the variable
|
|
(setq tp-test-watch-var "changed")
|
|
;; Check the layer is updated
|
|
(let ((props (cdr (assoc 'test-watch-layer tp-layer-alist))))
|
|
(should (equal (plist-get (plist-get props 'face) :foreground) "changed")))
|
|
;; Check the watcher was called
|
|
(should (= (length tp-test-watch-log) 1))
|
|
(let ((log-entry (car tp-test-watch-log)))
|
|
(should (equal (nth 0 log-entry) "changed"))
|
|
(should (equal (nth 1 log-entry) "initial"))
|
|
(should (eq (nth 2 log-entry) 'test-watch-layer))))
|
|
;; Cleanup
|
|
(makunbound 'tp-test-watch-var)
|
|
(makunbound 'tp-test-watch-log))))
|
|
|
|
(ert-deftest tp-test-define-layer-with-data ()
|
|
"Test reactive layers with :data for additional reactive variables."
|
|
(tp-test-with-temp-buffer
|
|
(unwind-protect
|
|
(progn
|
|
(define-tp test-data-layer () :props '(face (:foreground $tp-test-data-color))
|
|
:data '(tp-test-data-extra))
|
|
;; Check that variables were auto-defined
|
|
(should (boundp 'tp-test-data-color))
|
|
(should (boundp 'tp-test-data-extra))
|
|
;; Check data is registered
|
|
(should (assoc 'test-data-layer tp-layer-data))
|
|
;; Check the layer is defined
|
|
(should (assoc 'test-data-layer tp-layer-alist)))
|
|
;; Cleanup
|
|
(makunbound 'tp-test-data-color)
|
|
(makunbound 'tp-test-data-extra))))
|
|
|
|
(ert-deftest tp-test-define-layer-with-compute ()
|
|
"Test reactive layers with :compute for computed reactive variables."
|
|
(tp-test-with-temp-buffer
|
|
(unwind-protect
|
|
(progn
|
|
;; Set up the source variables
|
|
(setq tp-test-first-name "John")
|
|
(setq tp-test-last-name "Doe")
|
|
(define-tp test-compute-layer ()
|
|
:props '(help-echo $tp-test-full-name)
|
|
:data '(tp-test-first-name tp-test-last-name)
|
|
:compute '((tp-test-full-name
|
|
(lambda ()
|
|
(concat tp-test-first-name " " tp-test-last-name)))))
|
|
;; Check the layer is defined
|
|
(should (assoc 'test-compute-layer tp-layer-alist))
|
|
;; Check the computed is registered
|
|
(should (assoc 'test-compute-layer tp-layer-computed))
|
|
;; Check the computed variable has initial value
|
|
(should (equal tp-test-full-name "John Doe"))
|
|
;; Check the layer property uses the computed value
|
|
(let ((props (cdr (assoc 'test-compute-layer tp-layer-alist))))
|
|
(should (equal (plist-get props 'help-echo) "John Doe"))))
|
|
;; Cleanup
|
|
(makunbound 'tp-test-first-name)
|
|
(makunbound 'tp-test-last-name)
|
|
(makunbound 'tp-test-full-name))))
|
|
|
|
(ert-deftest tp-test-define-layer-with-data-and-compute ()
|
|
"Test reactive layers with :data and :compute together."
|
|
(tp-test-with-temp-buffer
|
|
(unwind-protect
|
|
(progn
|
|
;; Set data values first
|
|
(setq tp-test-dc-color "blue")
|
|
(setq tp-test-dc-first "Jane")
|
|
(setq tp-test-dc-last "Smith")
|
|
;; Define layer with :data and :compute
|
|
(define-tp test-dc-layer ()
|
|
:props '(face (:foreground $tp-test-dc-color) help-echo $tp-test-dc-full-name)
|
|
:data '(tp-test-dc-first tp-test-dc-last)
|
|
:compute '((tp-test-dc-full-name
|
|
(lambda ()
|
|
(concat tp-test-dc-first " " tp-test-dc-last)))))
|
|
;; Check data is registered
|
|
(should (assoc 'test-dc-layer tp-layer-data))
|
|
;; Check computed is registered
|
|
(should (assoc 'test-dc-layer tp-layer-computed))
|
|
;; Check the computed value
|
|
(should (equal tp-test-dc-full-name "Jane Smith")))
|
|
;; Cleanup
|
|
(ignore-errors (makunbound 'tp-test-dc-color))
|
|
(ignore-errors (makunbound 'tp-test-dc-first))
|
|
(ignore-errors (makunbound 'tp-test-dc-last))
|
|
(ignore-errors (makunbound 'tp-test-dc-full-name)))))
|
|
|
|
(ert-deftest tp-test-define-layer-watch-requires-props ()
|
|
"Test that :watch requires :props to be explicitly specified."
|
|
(tp-test-with-temp-buffer
|
|
(should-error
|
|
(define-tp test-invalid ()
|
|
:watch '((some-var (lambda (new old layer) nil)))))))
|
|
|
|
(ert-deftest tp-test-define-layer-compute-requires-props ()
|
|
"Test that :compute requires :props to be explicitly specified."
|
|
(tp-test-with-temp-buffer
|
|
(should-error
|
|
(define-tp test-invalid ()
|
|
:compute '((some-var (lambda () "computed")))))))
|
|
|
|
(ert-deftest tp-test-define-layer-data-requires-props ()
|
|
"Test that :data requires :props to be explicitly specified."
|
|
(tp-test-with-temp-buffer
|
|
(should-error
|
|
(define-tp test-invalid ()
|
|
:data '(some-var)))))
|
|
|
|
(ert-deftest tp-test-undefine-layer-clears-watch-compute-data ()
|
|
"Test tp-undefine-layer clears watchers, computed, and data."
|
|
(tp-test-with-temp-buffer
|
|
(unwind-protect
|
|
(progn
|
|
(define-tp test-undef-wcd ()
|
|
:props '(face (:foreground $tp-test-undef-color) help-echo $tp-test-undef-full)
|
|
:data '(tp-test-undef-first tp-test-undef-last)
|
|
:watch '((tp-test-undef-color (lambda (n o l) nil)))
|
|
:compute '((tp-test-undef-full
|
|
(lambda ()
|
|
(concat tp-test-undef-first " " tp-test-undef-last)))))
|
|
;; Check registrations
|
|
(should (assoc 'test-undef-wcd tp-layer-watchers))
|
|
(should (assoc 'test-undef-wcd tp-layer-computed))
|
|
(should (assoc 'test-undef-wcd tp-layer-data))
|
|
;; Undefine the layer
|
|
(tp-undefine-layer 'test-undef-wcd)
|
|
;; Check all are cleaned up
|
|
(should-not (assoc 'test-undef-wcd tp-layer-watchers))
|
|
(should-not (assoc 'test-undef-wcd tp-layer-computed))
|
|
(should-not (assoc 'test-undef-wcd tp-layer-data)))
|
|
;; Cleanup
|
|
(makunbound 'tp-test-undef-color)
|
|
(makunbound 'tp-test-undef-first)
|
|
(makunbound 'tp-test-undef-last)
|
|
(makunbound 'tp-test-undef-full))))
|
|
|
|
(ert-deftest tp-test-define-layer-group-with-watch ()
|
|
"Test define-tps with :watch (format-4)."
|
|
(tp-test-with-temp-buffer
|
|
(defvar tp-test-group-watch-var nil "Test variable for group watch.")
|
|
(defvar tp-test-group-watch-log nil "Log of watch callback invocations.")
|
|
(setq tp-test-group-watch-var "red")
|
|
(setq tp-test-group-watch-log nil)
|
|
(unwind-protect
|
|
(progn
|
|
(define-tps test-watch-group ()
|
|
'("reactive" :props (face (:foreground $tp-test-group-watch-var))
|
|
:watch ((tp-test-group-watch-var
|
|
(lambda (new old layer)
|
|
(push (list new old layer) tp-test-group-watch-log)))))
|
|
'("static" :props (face (:foreground "blue"))))
|
|
;; Check the group is defined
|
|
(should (assoc 'test-watch-group tp-layer-groups))
|
|
;; Check the reactive layer has its watcher registered
|
|
(should (assoc 'test-watch-group-reactive tp-layer-watchers))
|
|
;; Static layer should not have a watcher
|
|
(should-not (assoc 'test-watch-group-static tp-layer-watchers))
|
|
;; Change the variable
|
|
(setq tp-test-group-watch-var "green")
|
|
;; Check the watcher was called
|
|
(should (= (length tp-test-group-watch-log) 1)))
|
|
;; Cleanup
|
|
(makunbound 'tp-test-group-watch-var)
|
|
(makunbound 'tp-test-group-watch-log))))
|
|
|
|
(ert-deftest tp-test-reactive-reset-clears-all ()
|
|
"Test tp-reactive-reset clears watchers, computed, and data."
|
|
(tp-test-with-temp-buffer
|
|
(unwind-protect
|
|
(progn
|
|
(define-tp test-reset-all ()
|
|
:props '(face (:foreground $tp-test-reset-color) help-echo $tp-test-reset-full)
|
|
:data '(tp-test-reset-first tp-test-reset-last)
|
|
:watch '((tp-test-reset-color (lambda (n o l) nil)))
|
|
:compute '((tp-test-reset-full
|
|
(lambda ()
|
|
(concat tp-test-reset-first " " tp-test-reset-last)))))
|
|
;; Check registrations
|
|
(should tp-layer-watchers)
|
|
(should tp-layer-computed)
|
|
(should tp-layer-data)
|
|
;; Reset reactive
|
|
(tp-reactive-reset)
|
|
;; Check all are cleared
|
|
(should-not tp-layer-watchers)
|
|
(should-not tp-layer-computed)
|
|
(should-not tp-layer-data))
|
|
;; Cleanup - variables may or may not be bound
|
|
(ignore-errors (makunbound 'tp-test-reset-color))
|
|
(ignore-errors (makunbound 'tp-test-reset-first))
|
|
(ignore-errors (makunbound 'tp-test-reset-last))
|
|
(ignore-errors (makunbound 'tp-test-reset-full)))))
|
|
|
|
(ert-deftest tp-test-auto-define-variables ()
|
|
"Test that reactive variables are auto-defined when not bound."
|
|
(tp-test-with-temp-buffer
|
|
(unwind-protect
|
|
(progn
|
|
;; Variables should not exist before
|
|
(should-not (boundp 'tp-test-auto-var1))
|
|
(should-not (boundp 'tp-test-auto-var2))
|
|
(define-tp test-auto-layer () :props '(face (:foreground $tp-test-auto-var1))
|
|
:data '(tp-test-auto-var2))
|
|
;; Variables should now exist
|
|
(should (boundp 'tp-test-auto-var1))
|
|
(should (boundp 'tp-test-auto-var2)))
|
|
;; Cleanup
|
|
(makunbound 'tp-test-auto-var1)
|
|
(makunbound 'tp-test-auto-var2))))
|
|
|
|
(ert-deftest tp-test-setq-local-triggers-update ()
|
|
"Test that setq-local triggers reactive updates correctly."
|
|
(tp-test-with-temp-buffer
|
|
(unwind-protect
|
|
(progn
|
|
;; Define layer with auto-created variable (nil initial value)
|
|
(define-tp test-local-layer () :props '(face (:foreground $tp-test-local-color)))
|
|
;; Apply layer to text
|
|
(insert "Hello World")
|
|
(tp-set 1 6 'test-local-layer)
|
|
;; Initial value should be nil
|
|
(should (equal (plist-get (get-text-property 1 'face) :foreground) nil))
|
|
;; Use setq-local to set the value
|
|
(setq-local tp-test-local-color "red")
|
|
;; Text property should be updated
|
|
(should (equal (plist-get (get-text-property 1 'face) :foreground) "red")))
|
|
;; Cleanup
|
|
(makunbound 'tp-test-local-color))))
|
|
|
|
(ert-deftest tp-test-data-setq-local-triggers-compute ()
|
|
"Test that setq-local on :data variables triggers computed value updates."
|
|
(tp-test-with-temp-buffer
|
|
(unwind-protect
|
|
(progn
|
|
;; Define layer with :data and :compute
|
|
(define-tp test-data-compute-layer ()
|
|
:props '(help-echo $tp-test-dc-full)
|
|
:data '(tp-test-dc-first tp-test-dc-last)
|
|
:compute '((tp-test-dc-full
|
|
(lambda ()
|
|
(concat tp-test-dc-first " " tp-test-dc-last)))))
|
|
;; Apply layer to text
|
|
(insert "Hello World")
|
|
(tp-set 1 6 'test-data-compute-layer)
|
|
;; Initial computed value should be " " (concat nil nil = " ")
|
|
(should (equal (get-text-property 1 'help-echo) " "))
|
|
;; Use setq-local to set first name
|
|
(setq-local tp-test-dc-first "Kinney")
|
|
;; Computed should be "Kinney " now
|
|
(should (equal (get-text-property 1 'help-echo) "Kinney "))
|
|
;; Use setq-local to set last name
|
|
(setq-local tp-test-dc-last "Zhang")
|
|
;; Computed should be "Kinney Zhang" now
|
|
(should (equal (get-text-property 1 'help-echo) "Kinney Zhang")))
|
|
;; Cleanup
|
|
(ignore-errors (makunbound 'tp-test-dc-first))
|
|
(ignore-errors (makunbound 'tp-test-dc-last))
|
|
(ignore-errors (makunbound 'tp-test-dc-full)))))
|
|
|
|
(ert-deftest tp-test-data-with-initial-values ()
|
|
"Test that :data supports initial values with cons cell format."
|
|
(tp-test-with-temp-buffer
|
|
(unwind-protect
|
|
(progn
|
|
;; Define layer with :data having initial values
|
|
(define-tp test-data-init-layer ()
|
|
:props '(face (:foreground $tp-test-init-color) help-echo $tp-test-init-name)
|
|
:data '((tp-test-init-color . "blue")
|
|
(tp-test-init-name . "Initial Name")
|
|
tp-test-init-other))
|
|
;; Check initial values
|
|
(should (equal tp-test-init-color "blue"))
|
|
(should (equal tp-test-init-name "Initial Name"))
|
|
(should (equal tp-test-init-other nil))
|
|
;; Apply layer to text
|
|
(insert "Hello World")
|
|
(tp-set 1 6 'test-data-init-layer)
|
|
;; Check text properties have initial values
|
|
(should (equal (plist-get (get-text-property 1 'face) :foreground) "blue"))
|
|
(should (equal (get-text-property 1 'help-echo) "Initial Name")))
|
|
;; Cleanup
|
|
(ignore-errors (makunbound 'tp-test-init-color))
|
|
(ignore-errors (makunbound 'tp-test-init-name))
|
|
(ignore-errors (makunbound 'tp-test-init-other)))))
|
|
|
|
(ert-deftest tp-test-setq-local-only-updates-current-buffer ()
|
|
"Test that setq-local only updates text properties in the current buffer."
|
|
(let ((buf1 nil)
|
|
(buf2 nil))
|
|
(unwind-protect
|
|
(progn
|
|
;; Define a reactive layer
|
|
(define-tp test-multi-buf-layer () :props '(face (:foreground $tp-test-multi-color)))
|
|
;; Create first buffer with layer applied
|
|
(setq buf1 (generate-new-buffer " *test-buf1*"))
|
|
(with-current-buffer buf1
|
|
(insert "Hello World")
|
|
(tp-set 1 6 'test-multi-buf-layer))
|
|
;; Create second buffer with layer applied
|
|
(setq buf2 (generate-new-buffer " *test-buf2*"))
|
|
(with-current-buffer buf2
|
|
(insert "Hello World")
|
|
(tp-set 1 6 'test-multi-buf-layer))
|
|
;; Use setq-local in buf1
|
|
(with-current-buffer buf1
|
|
(setq-local tp-test-multi-color "red"))
|
|
;; buf1 should be updated
|
|
(with-current-buffer buf1
|
|
(should (equal (plist-get (get-text-property 1 'face) :foreground) "red")))
|
|
;; buf2 should NOT be updated (still nil)
|
|
(with-current-buffer buf2
|
|
(should (equal (plist-get (get-text-property 1 'face) :foreground) nil))))
|
|
;; Cleanup
|
|
(when (buffer-live-p buf1) (kill-buffer buf1))
|
|
(when (buffer-live-p buf2) (kill-buffer buf2))
|
|
(ignore-errors (makunbound 'tp-test-multi-color)))))
|
|
|
|
(ert-deftest tp-test-setq-updates-all-buffers-with-property ()
|
|
"Test that setq updates text properties in all buffers that have the property."
|
|
(let ((buf1 nil)
|
|
(buf2 nil))
|
|
(unwind-protect
|
|
(progn
|
|
;; Define a reactive layer
|
|
(define-tp test-global-layer () :props '(face (:foreground $tp-test-global-color)))
|
|
;; Create first buffer with layer applied
|
|
(setq buf1 (generate-new-buffer " *test-buf1*"))
|
|
(with-current-buffer buf1
|
|
(insert "Hello World")
|
|
(tp-set 1 6 'test-global-layer))
|
|
;; Create second buffer with layer applied
|
|
(setq buf2 (generate-new-buffer " *test-buf2*"))
|
|
(with-current-buffer buf2
|
|
(insert "Hello World")
|
|
(tp-set 1 6 'test-global-layer))
|
|
;; Use global setq
|
|
(setq tp-test-global-color "blue")
|
|
;; Both buffers should be updated
|
|
(with-current-buffer buf1
|
|
(should (equal (plist-get (get-text-property 1 'face) :foreground) "blue")))
|
|
(with-current-buffer buf2
|
|
(should (equal (plist-get (get-text-property 1 'face) :foreground) "blue"))))
|
|
;; Cleanup
|
|
(when (buffer-live-p buf1) (kill-buffer buf1))
|
|
(when (buffer-live-p buf2) (kill-buffer buf2))
|
|
(ignore-errors (makunbound 'tp-test-global-color)))))
|
|
|
|
;;; ============================================================
|
|
;;; Re-definition Tests (Issue: define-tp should update all properties on re-execution)
|
|
;;; ============================================================
|
|
|
|
(ert-deftest tp-test-redefine-layer-updates-data-initial-values ()
|
|
"Test that re-defining a layer with different :data initial values updates the variable."
|
|
(tp-test-with-temp-buffer
|
|
(unwind-protect
|
|
(progn
|
|
;; First definition with gray color
|
|
(define-tp test-redef-layer () :props '(face (:background $tp-test-redef-color))
|
|
:data '((tp-test-redef-color . "gray")))
|
|
;; Check initial value
|
|
(should (equal tp-test-redef-color "gray"))
|
|
;; Check layer props
|
|
(let ((props (cdr (assoc 'test-redef-layer tp-layer-alist))))
|
|
(should (equal (plist-get (plist-get props 'face) :background) "gray")))
|
|
;; Re-define with different color
|
|
(define-tp test-redef-layer () :props '(face (:background $tp-test-redef-color))
|
|
:data '((tp-test-redef-color . "blue")))
|
|
;; Check variable is updated
|
|
(should (equal tp-test-redef-color "blue"))
|
|
;; Check layer props are updated
|
|
(let ((props (cdr (assoc 'test-redef-layer tp-layer-alist))))
|
|
(should (equal (plist-get (plist-get props 'face) :background) "blue"))))
|
|
;; Cleanup
|
|
(ignore-errors (makunbound 'tp-test-redef-color)))))
|
|
|
|
(ert-deftest tp-test-redefine-layer-updates-props ()
|
|
"Test that re-defining a layer updates :props correctly."
|
|
(tp-test-with-temp-buffer
|
|
(unwind-protect
|
|
(progn
|
|
;; First definition
|
|
(define-tp test-redef-props () :props '(face (:foreground $tp-test-redef-fg))
|
|
:data '((tp-test-redef-fg . "red")))
|
|
(let ((props (cdr (assoc 'test-redef-props tp-layer-alist))))
|
|
(should (equal (plist-get (plist-get props 'face) :foreground) "red")))
|
|
;; Re-define with different props structure
|
|
(define-tp test-redef-props ()
|
|
:props '(face (:background $tp-test-redef-bg) help-echo "new")
|
|
:data '((tp-test-redef-bg . "yellow")))
|
|
;; Check new props are applied
|
|
(let ((props (cdr (assoc 'test-redef-props tp-layer-alist))))
|
|
(should (equal (plist-get (plist-get props 'face) :background) "yellow"))
|
|
(should (equal (plist-get props 'help-echo) "new"))
|
|
;; Old :foreground should NOT be present
|
|
(should (null (plist-get (plist-get props 'face) :foreground)))))
|
|
;; Cleanup
|
|
(ignore-errors (makunbound 'tp-test-redef-fg))
|
|
(ignore-errors (makunbound 'tp-test-redef-bg)))))
|
|
|
|
(ert-deftest tp-test-redefine-layer-clears-old-reactive-deps ()
|
|
"Test that re-defining a layer with different reactive vars clears old dependencies."
|
|
(tp-test-with-temp-buffer
|
|
(unwind-protect
|
|
(progn
|
|
;; First definition with $old-var
|
|
(define-tp test-redef-deps () :props '(face (:foreground $tp-test-old-var))
|
|
:data '((tp-test-old-var . "red")))
|
|
;; Check old var is in dependencies
|
|
(should (assoc 'tp-test-old-var tp-reactive-deps))
|
|
(let ((deps (cdr (assoc 'tp-test-old-var tp-reactive-deps))))
|
|
(should (assoc 'test-redef-deps deps)))
|
|
;; Re-define with $new-var
|
|
(define-tp test-redef-deps () :props '(face (:foreground $tp-test-new-var))
|
|
:data '((tp-test-new-var . "blue")))
|
|
;; Check old var is no longer in dependencies for this layer
|
|
(when-let ((deps (cdr (assoc 'tp-test-old-var tp-reactive-deps))))
|
|
(should-not (assoc 'test-redef-deps deps)))
|
|
;; Check new var is in dependencies
|
|
(should (assoc 'tp-test-new-var tp-reactive-deps))
|
|
(let ((deps (cdr (assoc 'tp-test-new-var tp-reactive-deps))))
|
|
(should (assoc 'test-redef-deps deps))))
|
|
;; Cleanup
|
|
(ignore-errors (makunbound 'tp-test-old-var))
|
|
(ignore-errors (makunbound 'tp-test-new-var)))))
|
|
|
|
(ert-deftest tp-test-redefine-layer-updates-watchers ()
|
|
"Test that re-defining a layer updates :watch correctly."
|
|
(tp-test-with-temp-buffer
|
|
;; Use defvar to create dynamically-bound variables that watcher callbacks can access
|
|
(defvar tp-test-watch-log-old nil "Log for old watcher.")
|
|
(defvar tp-test-watch-log-new nil "Log for new watcher.")
|
|
(setq tp-test-watch-log-old nil)
|
|
(setq tp-test-watch-log-new nil)
|
|
(unwind-protect
|
|
(progn
|
|
;; First definition with old watcher
|
|
(define-tp test-redef-watch () :props '(face (:foreground $tp-test-watch-var))
|
|
:data '((tp-test-watch-var . "red"))
|
|
:watch '((tp-test-watch-var
|
|
(lambda (new old layer)
|
|
(push (list 'old new) tp-test-watch-log-old)))))
|
|
;; Re-define with new watcher
|
|
(define-tp test-redef-watch () :props '(face (:foreground $tp-test-watch-var))
|
|
:data '((tp-test-watch-var . "red"))
|
|
:watch '((tp-test-watch-var
|
|
(lambda (new old layer)
|
|
(push (list 'new new) tp-test-watch-log-new)))))
|
|
;; Change variable
|
|
(setq tp-test-watch-var "blue")
|
|
;; Old watcher should NOT be called
|
|
(should (null tp-test-watch-log-old))
|
|
;; New watcher should be called
|
|
(should (= (length tp-test-watch-log-new) 1))
|
|
(should (equal (car tp-test-watch-log-new) '(new "blue"))))
|
|
;; Cleanup
|
|
(ignore-errors (makunbound 'tp-test-watch-var))
|
|
(makunbound 'tp-test-watch-log-old)
|
|
(makunbound 'tp-test-watch-log-new))))
|
|
|
|
(ert-deftest tp-test-redefine-layer-updates-compute ()
|
|
"Test that re-defining a layer updates :compute correctly."
|
|
(tp-test-with-temp-buffer
|
|
(unwind-protect
|
|
(progn
|
|
;; First definition with old compute
|
|
(setq tp-test-compute-src "hello")
|
|
(define-tp test-redef-compute ()
|
|
:props '(help-echo $tp-test-compute-out)
|
|
:data '(tp-test-compute-src)
|
|
:compute '((tp-test-compute-out
|
|
(lambda () (upcase tp-test-compute-src)))))
|
|
(should (equal tp-test-compute-out "HELLO"))
|
|
;; Re-define with different compute
|
|
(define-tp test-redef-compute ()
|
|
:props '(help-echo $tp-test-compute-out)
|
|
:data '(tp-test-compute-src)
|
|
:compute '((tp-test-compute-out
|
|
(lambda () (concat tp-test-compute-src "-suffix")))))
|
|
;; Check compute is updated
|
|
(should (equal tp-test-compute-out "hello-suffix"))
|
|
;; Trigger re-compute by changing source
|
|
(setq tp-test-compute-src "world")
|
|
(should (equal tp-test-compute-out "world-suffix")))
|
|
;; Cleanup
|
|
(ignore-errors (makunbound 'tp-test-compute-src))
|
|
(ignore-errors (makunbound 'tp-test-compute-out)))))
|
|
|
|
(ert-deftest tp-test-redefine-layer-from-reactive-to-static ()
|
|
"Test re-defining a layer from reactive to non-reactive clears dependencies."
|
|
(tp-test-with-temp-buffer
|
|
(unwind-protect
|
|
(progn
|
|
;; First definition with reactive variable
|
|
(define-tp test-reactive-to-static () :props '(face (:foreground $tp-test-r2s-color))
|
|
:data '((tp-test-r2s-color . "red")))
|
|
;; Check reactive dependency is registered
|
|
(should (assoc 'tp-test-r2s-color tp-reactive-deps))
|
|
;; Re-define as static (non-reactive)
|
|
(define-tp test-reactive-to-static () '(face bold))
|
|
;; Check reactive dependency is cleared
|
|
(when-let ((deps (cdr (assoc 'tp-test-r2s-color tp-reactive-deps))))
|
|
(should-not (assoc 'test-reactive-to-static deps)))
|
|
;; Check layer has new static props
|
|
(let ((props (tp-layer-props 'test-reactive-to-static)))
|
|
(should (eq (plist-get props 'face) 'bold))))
|
|
;; Cleanup
|
|
(ignore-errors (makunbound 'tp-test-r2s-color)))))
|
|
|
|
(ert-deftest tp-test-redefine-layer-group-updates-data ()
|
|
"Test that re-defining a layer group with different :data initial values updates the variable."
|
|
(tp-test-with-temp-buffer
|
|
(unwind-protect
|
|
(progn
|
|
;; First definition
|
|
(define-tps test-redef-group ()
|
|
'("layer1" :props (face (:background $tp-test-group-color))
|
|
:data ((tp-test-group-color . "gray"))))
|
|
;; Check initial value
|
|
(should (equal tp-test-group-color "gray"))
|
|
;; Re-define with different color
|
|
(define-tps test-redef-group ()
|
|
'("layer1" :props (face (:background $tp-test-group-color))
|
|
:data ((tp-test-group-color . "blue"))))
|
|
;; Check variable is updated
|
|
(should (equal tp-test-group-color "blue"))
|
|
;; Check layer props are updated
|
|
(let ((props (cdr (assoc 'test-redef-group-layer1 tp-layer-alist))))
|
|
(should (equal (plist-get (plist-get props 'face) :background) "blue"))))
|
|
;; Cleanup
|
|
(ignore-errors (makunbound 'tp-test-group-color)))))
|
|
|
|
(ert-deftest tp-test-redefine-applied-layer-updates-text ()
|
|
"Test that re-defining a layer updates text regions that have it applied."
|
|
(tp-test-with-temp-buffer
|
|
(unwind-protect
|
|
(progn
|
|
;; First definition
|
|
(define-tp test-redef-applied () :props '(face (:background $tp-test-applied-color))
|
|
:data '((tp-test-applied-color . "gray")))
|
|
;; Apply to text
|
|
(insert "Hello World")
|
|
(tp-set 1 6 'test-redef-applied)
|
|
;; Check initial color
|
|
(should (equal (plist-get (get-text-property 1 'face) :background) "gray"))
|
|
;; Re-define with different color
|
|
(define-tp test-redef-applied () :props '(face (:background $tp-test-applied-color))
|
|
:data '((tp-test-applied-color . "blue")))
|
|
;; The text should now have the new color
|
|
;; This happens because define-tp calls tp--update-layer-regions
|
|
;; at the end to update all text regions with the new properties
|
|
(should (equal (plist-get (get-text-property 1 'face) :background) "blue")))
|
|
;; Cleanup
|
|
(ignore-errors (makunbound 'tp-test-applied-color)))))
|
|
|
|
;;; ============================================================
|
|
;;; Reactive Text (tp-text) Tests
|
|
;;; ============================================================
|
|
|
|
(ert-deftest tp-test-tp-text-nil-initializes-to-current-text ()
|
|
"Test that tp-text with nil value is initialized to current text."
|
|
(tp-test-with-temp-buffer
|
|
(insert "Hello World")
|
|
(tp-set 1 6 '(face bold tp-text nil))
|
|
;; tp-text should be set to the current text
|
|
(should (equal (tp-at 1 'tp-text) "Hello"))
|
|
;; face should still be bold
|
|
(should (eq (tp-at 1 'face) 'bold))))
|
|
|
|
(ert-deftest tp-test-tp-text-string-object-replaces-content ()
|
|
"Test that tp-text on string object replaces the string content."
|
|
;; When tp-text is set on a string, the returned string should have
|
|
;; the tp-text value as its content, not the original string
|
|
(let ((result (tp-set "2" 'face '(:background "green") 'tp-text "6")))
|
|
;; The returned string should be "6", not "2"
|
|
(should (equal result "6"))
|
|
;; Properties should be applied
|
|
(should (equal (get-text-property 0 'face result) '(:background "green")))
|
|
(should (equal (get-text-property 0 'tp-text result) "6"))))
|
|
|
|
(ert-deftest tp-test-tp-text-string-replaces-text ()
|
|
"Test that tp-text with string value replaces the text in the region."
|
|
(tp-test-with-temp-buffer
|
|
(insert "Hello World")
|
|
(tp-set 1 6 '(face bold tp-text "Hi"))
|
|
;; Text should be replaced
|
|
(should (equal (buffer-substring-no-properties 1 3) "Hi"))
|
|
;; face should still be applied
|
|
(should (eq (tp-at 1 'face) 'bold))
|
|
;; tp-text property should be set
|
|
(should (equal (tp-at 1 'tp-text) "Hi"))))
|
|
|
|
(ert-deftest tp-test-tp-text-preserves-other-properties ()
|
|
"Test that tp-text replacement preserves existing properties."
|
|
(tp-test-with-temp-buffer
|
|
(insert "Hello World")
|
|
;; First set some properties
|
|
(tp-set 1 6 '(help-echo "greeting"))
|
|
;; Then set tp-text with face
|
|
(tp-set 1 6 '(face bold tp-text "Hi"))
|
|
;; Text should be replaced
|
|
(should (equal (buffer-substring-no-properties 1 3) "Hi"))
|
|
;; Both face and help-echo should be preserved
|
|
(should (eq (tp-at 1 'face) 'bold))
|
|
(should (equal (tp-at 1 'help-echo) "greeting"))))
|
|
|
|
(ert-deftest tp-test-tp-reset-with-tp-text ()
|
|
"Test that tp-reset with tp-text works correctly."
|
|
(tp-test-with-temp-buffer
|
|
(insert "Hello World")
|
|
(tp-reset 1 6 '(face italic tp-text "Bye"))
|
|
;; Text should be replaced
|
|
(should (equal (buffer-substring-no-properties 1 4) "Bye"))
|
|
;; Properties should be set
|
|
(should (eq (tp-at 1 'face) 'italic))
|
|
(should (equal (tp-at 1 'tp-text) "Bye"))))
|
|
|
|
(ert-deftest tp-test-tp-add-with-tp-text ()
|
|
"Test that tp-add with tp-text works correctly."
|
|
(tp-test-with-temp-buffer
|
|
(insert "Hello World")
|
|
(tp-set 1 6 '(help-echo "existing"))
|
|
(tp-add 1 6 '(face bold tp-text "Hi"))
|
|
;; Text should be replaced
|
|
(should (equal (buffer-substring-no-properties 1 3) "Hi"))
|
|
;; Both properties should be present
|
|
(should (eq (tp-at 1 'face) 'bold))
|
|
(should (equal (tp-at 1 'help-echo) "existing"))))
|
|
|
|
(ert-deftest tp-test-tp-text-reactive-layer ()
|
|
"Test tp-text with reactive variable."
|
|
(tp-test-with-temp-buffer
|
|
(defvar tp-test-reactive-text nil "Test variable for reactive text.")
|
|
(setq tp-test-reactive-text "Initial")
|
|
(unwind-protect
|
|
(progn
|
|
(define-tp test-reactive-text-layer ()
|
|
:props '(face bold tp-text $tp-test-reactive-text))
|
|
;; Apply layer to text
|
|
(insert "Hello World")
|
|
(tp-set 1 6 'test-reactive-text-layer)
|
|
;; Initial text should be replaced
|
|
(should (equal (buffer-substring-no-properties 1 8) "Initial"))
|
|
;; tp-text should be set
|
|
(should (equal (tp-at 1 'tp-text) "Initial"))
|
|
;; Change the reactive variable
|
|
(setq tp-test-reactive-text "Changed")
|
|
;; Text should be updated
|
|
(should (equal (buffer-substring-no-properties 1 8) "Changed"))
|
|
;; face should still be applied
|
|
(should (eq (tp-at 1 'face) 'bold)))
|
|
;; Cleanup
|
|
(makunbound 'tp-test-reactive-text))))
|
|
|
|
(ert-deftest tp-test-tp-text-reactive-nil-initializes-variable ()
|
|
"Test tp-text with nil reactive variable initializes the variable to source text.
|
|
When tp-text is bound to a reactive variable and that variable is nil,
|
|
the source text should be used and the reactive variable should be updated."
|
|
(tp-test-with-temp-buffer
|
|
(defvar tp-test-text-var nil "Test variable for tp-text initialization.")
|
|
(setq tp-test-text-var nil)
|
|
(unwind-protect
|
|
(progn
|
|
;; Define layer with tp-text bound to a reactive variable
|
|
(define-tp test-init-text-layer ()
|
|
:props '(face bold tp-text $tp-test-text-var))
|
|
;; Apply layer to string - variable is nil, so source text should be used
|
|
(let ((result (tp-set "2" 'test-init-text-layer)))
|
|
;; Result should be the source text "2"
|
|
(should (equal result "2"))
|
|
;; tp-test-text-var should now be "2"
|
|
(should (equal tp-test-text-var "2"))
|
|
;; tp-text property should be "2"
|
|
(should (equal (tp-at 0 'tp-text result) "2")))
|
|
;; Now set the variable to a different value and test again
|
|
(setq tp-test-text-var "18")
|
|
;; Redefine layer to reset resolved props to the new variable value.
|
|
;; This is necessary because the layer definition caches the resolved
|
|
;; tp-text value, and we want to test the behavior when the variable
|
|
;; already has a non-nil value at layer application time.
|
|
(define-tp test-init-text-layer ()
|
|
:props '(face bold tp-text $tp-test-text-var))
|
|
(let ((result (tp-set "2" 'test-init-text-layer)))
|
|
;; Result should be the variable value "18", not source "2"
|
|
(should (equal result "18"))
|
|
;; Variable should remain "18"
|
|
(should (equal tp-test-text-var "18"))))
|
|
;; Cleanup
|
|
(makunbound 'tp-test-text-var))))
|
|
|
|
(ert-deftest tp-test-tp-text-direct-string-uses-specified-text ()
|
|
"Test tp-text with direct string value uses that string, not source text.
|
|
When tp-text is set directly to a string (not a reactive variable),
|
|
the inserted text should be that string, not the source text."
|
|
(let ((result (tp-set "2" 'tp-text "23")))
|
|
;; Result should be "23", not "2"
|
|
(should (equal result "23"))
|
|
;; tp-text property should be "23"
|
|
(should (equal (tp-at 0 'tp-text result) "23"))))
|
|
|
|
(ert-deftest tp-test-tp-text-reactive-computed ()
|
|
"Test tp-text with computed reactive variable."
|
|
(tp-test-with-temp-buffer
|
|
(unwind-protect
|
|
(progn
|
|
(setq tp-test-name-part1 "Hello")
|
|
(setq tp-test-name-part2 "World")
|
|
(define-tp test-computed-text-layer ()
|
|
:props '(face bold tp-text $tp-test-full-text)
|
|
:data '(tp-test-name-part1 tp-test-name-part2)
|
|
:compute '((tp-test-full-text
|
|
(lambda ()
|
|
(concat tp-test-name-part1 " " tp-test-name-part2)))))
|
|
;; Apply layer to text
|
|
(insert "placeholder")
|
|
(tp-set 1 12 'test-computed-text-layer)
|
|
;; Text should be replaced with computed value
|
|
(should (equal (buffer-substring-no-properties 1 12) "Hello World"))
|
|
;; Change a data variable
|
|
(setq tp-test-name-part1 "Goodbye")
|
|
;; Text should be updated with new computed value
|
|
(should (equal (buffer-substring-no-properties 1 14) "Goodbye World")))
|
|
;; Cleanup
|
|
(ignore-errors (makunbound 'tp-test-name-part1))
|
|
(ignore-errors (makunbound 'tp-test-name-part2))
|
|
(ignore-errors (makunbound 'tp-test-full-text)))))
|
|
|
|
(ert-deftest tp-test-tp-text-same-text-different-properties ()
|
|
"Test tp-text updates when text is same but properties differ.
|
|
When the reactive variable changes to a propertized string with the same
|
|
text content but different properties, the properties should be updated."
|
|
(tp-test-with-temp-buffer
|
|
(defvar tp-test-same-text nil "Test variable for same text different props.")
|
|
(setq tp-test-same-text "emacs")
|
|
(unwind-protect
|
|
(progn
|
|
(define-tp test-same-text-layer ()
|
|
:props '(face (:foreground "green") tp-text $tp-test-same-text))
|
|
;; Apply layer to text - insert placeholder and apply layer to entire buffer
|
|
(insert "placeholder")
|
|
(tp-set (point-min) (point-max) 'test-same-text-layer)
|
|
;; Initial text should be "emacs" with foreground green
|
|
(should (equal (buffer-substring-no-properties (point-min) (point-max)) "emacs"))
|
|
(should (equal (plist-get (tp-at (point-min) 'face) :foreground) "green"))
|
|
;; Change the reactive variable to same text but different properties
|
|
(setq tp-test-same-text (propertize "emacs" 'face 'bold))
|
|
;; Text should still be "emacs"
|
|
(should (equal (buffer-substring-no-properties (point-min) (point-max)) "emacs"))
|
|
;; Face should now include bold from the propertized string
|
|
(let ((face-val (tp-at (point-min) 'face)))
|
|
(should (or (eq face-val 'bold)
|
|
(and (listp face-val) (memq 'bold face-val))))))
|
|
;; Cleanup
|
|
(makunbound 'tp-test-same-text))))
|
|
|
|
;;; ============================================================
|
|
;;; tp-text with Embedded Text Properties Tests
|
|
;;; ============================================================
|
|
|
|
(ert-deftest tp-test-tp-text-with-embedded-properties-string ()
|
|
"Test that tp-set with tp-text preserves embedded properties on strings."
|
|
;; When tp-text is a propertized string, embedded properties should be preserved
|
|
;; (props still override embedded if there's a conflict)
|
|
(let* ((propertized-text (copy-sequence "Hello"))
|
|
(_ (put-text-property 0 5 'custom-prop 'embedded-value propertized-text))
|
|
(result (tp-set "X" 'tp-text propertized-text 'face 'bold)))
|
|
;; The text content should be from tp-text
|
|
(should (equal result "Hello"))
|
|
;; The face property from props should be applied
|
|
(should (equal (tp-at 0 'face result) 'bold))
|
|
;; tp-set now preserves embedded props
|
|
(should (equal (tp-at 0 'custom-prop result) 'embedded-value))))
|
|
|
|
(ert-deftest tp-test-tp-add-with-embedded-properties-string ()
|
|
"Test that tp-add with tp-text merges embedded properties on strings."
|
|
;; When tp-text is a propertized string and tp-add is used, props are merged
|
|
(let* ((propertized-text (copy-sequence "Hello"))
|
|
(_ (put-text-property 0 5 'custom-prop 'embedded-value propertized-text))
|
|
(result (tp-add "X" 'tp-text propertized-text 'face 'bold)))
|
|
;; The text content should be from tp-text
|
|
(should (equal result "Hello"))
|
|
;; The face property from props should be applied
|
|
(should (equal (tp-at 0 'face result) 'bold))
|
|
;; tp-add merges - embedded custom-prop should be present
|
|
(should (equal (tp-at 0 'custom-prop result) 'embedded-value))))
|
|
|
|
(ert-deftest tp-test-tp-text-with-embedded-face-string ()
|
|
"Test that tp-set with tp-text preserves embedded face on strings."
|
|
(let* ((propertized-text (copy-sequence "Hello"))
|
|
(_ (put-text-property 0 5 'face 'italic propertized-text))
|
|
;; Set tp-text with its own face, and also specify help-echo
|
|
(result (tp-set "X" 'tp-text propertized-text 'help-echo "tip")))
|
|
;; The text content should be from tp-text
|
|
(should (equal result "Hello"))
|
|
;; tp-set now preserves embedded face
|
|
(should (equal (tp-at 0 'face result) 'italic))
|
|
;; The help-echo from props should be applied
|
|
(should (equal (tp-at 0 'help-echo result) "tip"))))
|
|
|
|
(ert-deftest tp-test-tp-add-with-embedded-face-string ()
|
|
"Test that tp-add with tp-text merges embedded face on strings."
|
|
(let* ((propertized-text (copy-sequence "Hello"))
|
|
(_ (put-text-property 0 5 'face 'italic propertized-text))
|
|
;; Add tp-text with its own face, and also specify help-echo
|
|
(result (tp-add "X" 'tp-text propertized-text 'help-echo "tip")))
|
|
;; The text content should be from tp-text
|
|
(should (equal result "Hello"))
|
|
;; tp-add merges - embedded face should be present
|
|
(should (equal (tp-at 0 'face result) 'italic))
|
|
;; The help-echo from props should be applied
|
|
(should (equal (tp-at 0 'help-echo result) "tip"))))
|
|
|
|
(ert-deftest tp-test-tp-text-with-embedded-properties-buffer ()
|
|
"Test that tp-set with tp-text preserves embedded properties in buffers."
|
|
(tp-test-with-temp-buffer
|
|
(insert "Original")
|
|
(let* ((propertized-text (copy-sequence "New"))
|
|
(_ (put-text-property 0 3 'custom-prop 'embedded-value propertized-text)))
|
|
(tp-set 1 9 `(face bold tp-text ,propertized-text))
|
|
;; The text content should be replaced with tp-text value
|
|
(should (equal (buffer-substring-no-properties 1 4) "New"))
|
|
;; The face from props should be applied
|
|
(should (equal (tp-at 1 'face) 'bold))
|
|
;; tp-set now preserves embedded props
|
|
(should (equal (tp-at 1 'custom-prop) 'embedded-value)))))
|
|
|
|
(ert-deftest tp-test-tp-text-with-mixed-properties ()
|
|
"Test that tp-set with tp-text preserves embedded properties."
|
|
;; tp-set now preserves embedded props (props still take precedence for conflicts)
|
|
(let* ((propertized-text (copy-sequence "ABCD"))
|
|
;; Set a property at position 0
|
|
(_ (put-text-property 0 4 'region-type 'start propertized-text))
|
|
(result (tp-set "X" 'tp-text propertized-text 'face 'bold)))
|
|
;; The text content should be from tp-text
|
|
(should (equal result "ABCD"))
|
|
;; The face from props should be applied uniformly
|
|
(should (equal (tp-at 0 'face result) 'bold))
|
|
(should (equal (tp-at 3 'face result) 'bold))
|
|
;; tp-set now preserves embedded props
|
|
(should (equal (tp-at 0 'region-type result) 'start))))
|
|
|
|
(ert-deftest tp-test-tp-reset-with-embedded-properties ()
|
|
"Test that tp-reset preserves embedded text properties from tp-text."
|
|
(let* ((propertized-text (copy-sequence "Test"))
|
|
(_ (put-text-property 0 4 'custom-prop 'value propertized-text))
|
|
(result (tp-reset "X" 'tp-text propertized-text 'face 'bold)))
|
|
;; The text content should be from tp-text
|
|
(should (equal result "Test"))
|
|
;; The face from props should be applied
|
|
(should (equal (tp-at 0 'face result) 'bold))
|
|
;; tp-reset now preserves embedded props from tp-text
|
|
(should (equal (tp-at 0 'custom-prop result) 'value))))
|
|
|
|
(ert-deftest tp-test-tp-add-with-embedded-properties ()
|
|
"Test that tp-add with embedded text properties preserves them."
|
|
(let* ((propertized-text (copy-sequence "Test"))
|
|
(_ (put-text-property 0 4 'custom-prop 'value propertized-text))
|
|
(result (tp-add "X" 'tp-text propertized-text 'face 'bold)))
|
|
;; The text content should be from tp-text
|
|
(should (equal result "Test"))
|
|
;; The face from props should be applied
|
|
(should (equal (tp-at 0 'face result) 'bold))
|
|
;; The embedded custom-prop from tp-text should be preserved
|
|
(should (equal (tp-at 0 'custom-prop result) 'value))))
|
|
|
|
(ert-deftest tp-test-tp-text-face-merging ()
|
|
"Test that tp-add with tp-text merges embedded face property with props face."
|
|
;; This is the core use case for tp-add: merging face 'bold with face (:foreground \"red\")
|
|
(let ((result (tp-add "emacs" 'face 'bold 'tp-text (propertize "vim" 'face '(:foreground "red")))))
|
|
;; Text should be replaced
|
|
(should (equal result "vim"))
|
|
;; Face should be merged: (:foreground \"red\") + bold
|
|
(let ((face-val (tp-at 0 'face result)))
|
|
;; Should contain both the plist and symbol
|
|
(should (member 'bold (if (listp face-val) face-val (list face-val))))
|
|
;; Should have foreground red
|
|
(should (or (equal face-val '(:foreground "red"))
|
|
(and (listp face-val)
|
|
(cl-some (lambda (f)
|
|
(and (listp f)
|
|
(equal (plist-get f :foreground) "red")))
|
|
face-val)))))))
|
|
|
|
(ert-deftest tp-test-tp-add-face-override-subprops ()
|
|
"Test that tp-add with tp-text overrides same face sub-properties."
|
|
;; When new props have same sub-property as embedded, new value should override
|
|
;; Example: new (:foreground "green") should override embedded (:foreground "red")
|
|
(let ((result (tp-add "emacs" 'face '(:foreground "green")
|
|
'tp-text (propertize "vim" 'face '(:foreground "red")))))
|
|
(should (equal result "vim"))
|
|
;; Face should be (:foreground "green") - new overrides old
|
|
(let ((face-val (tp-at 0 'face result)))
|
|
(should (equal face-val '(:foreground "green")))))
|
|
;; More complex case: new (bold (:foreground "green")) with embedded (:foreground "red")
|
|
(let ((result (tp-add "emacs" 'face '(bold (:foreground "green"))
|
|
'tp-text (propertize "vim" 'face '(:foreground "red")))))
|
|
(should (equal result "vim"))
|
|
;; Face should be (bold (:foreground "green")) - new overrides old
|
|
(let ((face-val (tp-at 0 'face result)))
|
|
(should (member 'bold (if (listp face-val) face-val (list face-val))))
|
|
;; Should have green, not red
|
|
(should (cl-some (lambda (f)
|
|
(and (listp f)
|
|
(keywordp (car-safe f))
|
|
(equal (plist-get f :foreground) "green")))
|
|
(if (and (listp face-val) (not (keywordp (car-safe face-val))))
|
|
face-val
|
|
(list face-val))))))
|
|
;; Mixed format case: new (bold :foreground "green") with embedded (:foreground "red")
|
|
(let ((result (tp-add "emacs" 'face '(bold :foreground "green")
|
|
'tp-text (propertize "vim" 'face '(:foreground "red")))))
|
|
(should (equal result "vim"))
|
|
;; Face should be (bold (:foreground "green")) - parsed correctly and new overrides old
|
|
(let ((face-val (tp-at 0 'face result)))
|
|
(should (member 'bold (if (listp face-val) face-val (list face-val))))
|
|
;; Should have green, not red
|
|
(should (cl-some (lambda (f)
|
|
(and (listp f)
|
|
(keywordp (car-safe f))
|
|
(equal (plist-get f :foreground) "green")))
|
|
(if (and (listp face-val) (not (keywordp (car-safe face-val))))
|
|
face-val
|
|
(list face-val)))))))
|
|
|
|
;;; ============================================================
|
|
;;; New define-tp Format Tests (Parameterized and Non-Parameterized)
|
|
;;; ============================================================
|
|
|
|
(ert-deftest tp-test-define-tp-non-parameterized ()
|
|
"Test define-tp with non-parameterized format (empty arglist)."
|
|
(tp-test-with-temp-buffer
|
|
(define-tp tp-bold ()
|
|
'(face bold))
|
|
(should (assoc 'tp-bold tp-layer-alist))
|
|
;; Unified structure: (LAYER-NAME nil BODY-FORM) where BODY-FORM is quoted
|
|
(let ((entry (cdr (assoc 'tp-bold tp-layer-alist))))
|
|
(should (= (length entry) 2))
|
|
(should (null (car entry))) ; arglist is nil
|
|
(should (equal (eval (cadr entry)) '(face bold))))))
|
|
|
|
(ert-deftest tp-test-define-tp-non-parameterized-usage-string ()
|
|
"Test non-parameterized layer usage with string: (tp-set string 'layer-name t).
|
|
When using tp-set (direct property setting), tp-name is NOT added."
|
|
(tp-test-with-temp-buffer
|
|
(define-tp tp-bold ()
|
|
'(face bold))
|
|
(let ((result (tp-set "emacs" 'tp-bold t)))
|
|
;; Result should have the correct properties
|
|
(should-not (get-text-property 0 'tp-name result)) ; no tp-name for direct setting
|
|
(should (eq (get-text-property 0 'face result) 'bold)))))
|
|
|
|
(ert-deftest tp-test-define-tp-non-parameterized-usage-region ()
|
|
"Test non-parameterized layer usage with region: (tp-set start end '(layer-name t)).
|
|
When using tp-set (direct property setting), tp-name is NOT added."
|
|
(tp-test-with-temp-buffer
|
|
(insert "emacs")
|
|
(define-tp tp-bold ()
|
|
'(face bold))
|
|
(tp-set 1 6 '(tp-bold t))
|
|
;; Check properties in buffer
|
|
(should-not (tp-at 1 'tp-name)) ; no tp-name for direct setting
|
|
(should (eq (tp-at 1 'face) 'bold))))
|
|
|
|
(ert-deftest tp-test-define-tp-parameterized ()
|
|
"Test define-tp with parameterized format."
|
|
(tp-test-with-temp-buffer
|
|
(define-tp tp-space (pixel)
|
|
(list 'display (list 'space :width (list pixel))))
|
|
;; Check it's registered as a parameterized layer in tp-layer-alist
|
|
(should (assoc 'tp-space tp-layer-alist))
|
|
(should (tp-layer-parameterized-p 'tp-space))
|
|
;; Check the structure is correct (ARGLIST BODY-FORM)
|
|
(let ((entry (cdr (assoc 'tp-space tp-layer-alist))))
|
|
;; entry is (ARGLIST BODY-FORM)
|
|
(should (equal (car entry) '(pixel))))))
|
|
|
|
(ert-deftest tp-test-define-tp-parameterized-usage-string ()
|
|
"Test parameterized layer usage with string: (tp-set string 'layer-name arg).
|
|
When using tp-set (direct property setting), tp-name is NOT added."
|
|
(tp-test-with-temp-buffer
|
|
(define-tp tp-space (pixel)
|
|
(list 'display (list 'space :width (list pixel))))
|
|
(let ((result (tp-set "emacs" 'tp-space 2)))
|
|
;; Result should have the correct properties
|
|
(should-not (get-text-property 0 'tp-name result)) ; no tp-name for direct setting
|
|
(should (equal (get-text-property 0 'display result) '(space :width (2)))))))
|
|
|
|
(ert-deftest tp-test-define-tp-parameterized-usage-region ()
|
|
"Test parameterized layer usage with region: (tp-set start end '(layer-name arg)).
|
|
When using tp-set (direct property setting), tp-name is NOT added."
|
|
(tp-test-with-temp-buffer
|
|
(insert "emacs")
|
|
(define-tp tp-space (pixel)
|
|
(list 'display (list 'space :width (list pixel))))
|
|
(tp-set 1 6 '(tp-space 5))
|
|
;; Check properties in buffer
|
|
(should-not (tp-at 1 'tp-name)) ; no tp-name for direct setting
|
|
(should (equal (tp-at 1 'display) '(space :width (5))))))
|
|
|
|
(ert-deftest tp-test-define-tp-parameterized-backquote ()
|
|
"Test parameterized layer with backquote syntax.
|
|
When using tp-set (direct property setting), tp-name is NOT added."
|
|
(tp-test-with-temp-buffer
|
|
(define-tp tp-test-space (pixel)
|
|
`(display (space :width (,pixel))))
|
|
(let ((result (tp-set "emacs" 'tp-test-space 10)))
|
|
(should-not (get-text-property 0 'tp-name result)) ; no tp-name for direct setting
|
|
(should (equal (get-text-property 0 'display result) '(space :width (10)))))))
|
|
|
|
(ert-deftest tp-test-define-tp-parameterized-undefine ()
|
|
"Test tp-undefine-layer clears parameterized layer info."
|
|
(tp-test-with-temp-buffer
|
|
(define-tp tp-test-param (arg)
|
|
(list 'display arg))
|
|
(should (assoc 'tp-test-param tp-layer-alist))
|
|
(should (tp-layer-parameterized-p 'tp-test-param))
|
|
(tp-undefine-layer 'tp-test-param)
|
|
(should-not (assoc 'tp-test-param tp-layer-alist))))
|
|
|
|
(ert-deftest tp-test-layer-reset-clears-params ()
|
|
"Test tp-layer-reset clears parameterized layers."
|
|
(tp-test-with-temp-buffer
|
|
(define-tp tp-test-param (arg)
|
|
(list 'display arg))
|
|
(should (assoc 'tp-test-param tp-layer-alist))
|
|
(tp-layer-reset)
|
|
(should-not tp-layer-alist)))
|
|
|
|
(ert-deftest tp-test-layer-with-extra-props-string ()
|
|
"Test layer with extra native properties on string.
|
|
When using tp-set (direct property setting), tp-name is NOT added."
|
|
(tp-test-with-temp-buffer
|
|
(define-tp tp-bold ()
|
|
'(face bold))
|
|
;; Non-parameterized layer with extra props
|
|
(let ((result (tp-set "emacs" 'tp-bold t 'face '(:foreground "green"))))
|
|
(should-not (get-text-property 0 'tp-name result)) ; no tp-name for direct setting
|
|
;; Should have both face values in the plist
|
|
(let ((props (text-properties-at 0 result)))
|
|
(should (member 'face props))))))
|
|
|
|
(ert-deftest tp-test-parameterized-layer-with-extra-props-string ()
|
|
"Test parameterized layer with extra native properties on string.
|
|
When using tp-set (direct property setting), tp-name is NOT added."
|
|
(tp-test-with-temp-buffer
|
|
(define-tp tp-space (pixel)
|
|
`(display (space :width (,pixel))))
|
|
;; Parameterized layer with extra props
|
|
(let ((result (tp-set "emacs" 'tp-space 6 'face '(:foreground "green"))))
|
|
(should-not (get-text-property 0 'tp-name result)) ; no tp-name for direct setting
|
|
(should (equal (get-text-property 0 'display result) '(space :width (6))))
|
|
(should (equal (get-text-property 0 'face result) '(:foreground "green"))))))
|
|
|
|
(ert-deftest tp-test-layer-with-extra-props-region ()
|
|
"Test layer with extra native properties on region.
|
|
When using tp-set (direct property setting), tp-name is NOT added."
|
|
(tp-test-with-temp-buffer
|
|
(define-tp tp-bold ()
|
|
'(face bold))
|
|
;; Region form with extra props
|
|
(let ((result (tp-set 0 5 '(tp-bold t face (:foreground "green")) "emacs")))
|
|
(should-not (get-text-property 0 'tp-name result)) ; no tp-name for direct setting
|
|
;; Should have both face values in the plist
|
|
(let ((props (text-properties-at 0 result)))
|
|
(should (member 'face props))))))
|
|
|
|
(ert-deftest tp-test-parameterized-layer-with-extra-props-region ()
|
|
"Test parameterized layer with extra native properties on region.
|
|
When using tp-set (direct property setting), tp-name is NOT added."
|
|
(tp-test-with-temp-buffer
|
|
(define-tp tp-space (pixel)
|
|
`(display (space :width (,pixel))))
|
|
;; Region form with extra props
|
|
(let ((result (tp-set 0 5 '(tp-space 6 face (:foreground "green")) "emacs")))
|
|
(should-not (get-text-property 0 'tp-name result)) ; no tp-name for direct setting
|
|
(should (equal (get-text-property 0 'display result) '(space :width (6))))
|
|
(should (equal (get-text-property 0 'face result) '(:foreground "green"))))))
|
|
|
|
(ert-deftest tp-test-layer-at-any-position-string ()
|
|
"Test layer properties can be at any position in string form.
|
|
When using tp-set (direct property setting), tp-name is NOT added."
|
|
(tp-test-with-temp-buffer
|
|
(define-tp tp-space (pixel)
|
|
`(display (space :width (,pixel))))
|
|
;; Layer in the middle of the plist
|
|
(let ((result (tp-set "emacs"
|
|
'face '(:foreground "green")
|
|
'tp-space 6
|
|
'test "test")))
|
|
(should-not (get-text-property 0 'tp-name result)) ; no tp-name for direct setting
|
|
(should (equal (get-text-property 0 'display result) '(space :width (6))))
|
|
(should (equal (get-text-property 0 'face result) '(:foreground "green")))
|
|
(should (equal (get-text-property 0 'test result) "test")))))
|
|
|
|
(ert-deftest tp-test-layer-at-any-position-region ()
|
|
"Test layer properties can be at any position in region form.
|
|
When using tp-set (direct property setting), tp-name is NOT added."
|
|
(tp-test-with-temp-buffer
|
|
(define-tp tp-space (pixel)
|
|
`(display (space :width (,pixel))))
|
|
;; Layer in the middle of the plist
|
|
(let ((result (tp-set 0 5 '(face (:foreground "green") tp-space 6 test "test") "emacs")))
|
|
(should-not (get-text-property 0 'tp-name result)) ; no tp-name for direct setting
|
|
(should (equal (get-text-property 0 'display result) '(space :width (6))))
|
|
(should (equal (get-text-property 0 'face result) '(:foreground "green")))
|
|
(should (equal (get-text-property 0 'test result) "test")))))
|
|
|
|
(ert-deftest tp-test-non-param-layer-at-any-position ()
|
|
"Test non-parameterized layer at any position.
|
|
When using tp-set (direct property setting), tp-name is NOT added."
|
|
(tp-test-with-temp-buffer
|
|
(define-tp tp-bold ()
|
|
'(face bold))
|
|
;; Layer in the middle of the plist
|
|
(let ((result (tp-set "emacs"
|
|
'test1 "value1"
|
|
'tp-bold t
|
|
'test2 "value2")))
|
|
(should-not (get-text-property 0 'tp-name result)) ; no tp-name for direct setting
|
|
(should (equal (get-text-property 0 'test1 result) "value1"))
|
|
(should (equal (get-text-property 0 'test2 result) "value2")))))
|
|
|
|
;; Tests for tp-push-layer and tp-put-layer with define-tp layers
|
|
(ert-deftest tp-test-push-layer-non-parameterized ()
|
|
"Test tp-push-layer with non-parameterized define-tp layer."
|
|
(tp-test-with-temp-buffer
|
|
(define-tp tp-bold ()
|
|
'(face bold))
|
|
;; String form
|
|
(let ((result (tp-push-layer "emacs" 'tp-bold)))
|
|
(should (eq (get-text-property 0 'tp-name result) 'tp-bold))
|
|
(should (eq (get-text-property 0 'face result) 'bold)))))
|
|
|
|
(ert-deftest tp-test-push-layer-parameterized ()
|
|
"Test tp-push-layer with parameterized define-tp layer."
|
|
(tp-test-with-temp-buffer
|
|
(define-tp tp-space (pixel)
|
|
`(display (space :width (,pixel))))
|
|
;; String form with parameterized layer
|
|
(let ((result (tp-push-layer "emacs" '(tp-space 6))))
|
|
(should (eq (get-text-property 0 'tp-name result) 'tp-space))
|
|
(should (equal (get-text-property 0 'display result) '(space :width (6)))))))
|
|
|
|
(ert-deftest tp-test-put-layer-non-parameterized ()
|
|
"Test tp-put-layer with non-parameterized define-tp layer."
|
|
(tp-test-with-temp-buffer
|
|
(define-tp tp-italic ()
|
|
'(face italic))
|
|
;; String form
|
|
(let ((result (tp-put-layer "emacs" 'tp-italic 0)))
|
|
(should (eq (get-text-property 0 'tp-name result) 'tp-italic))
|
|
(should (eq (get-text-property 0 'face result) 'italic)))))
|
|
|
|
(ert-deftest tp-test-put-layer-parameterized ()
|
|
"Test tp-put-layer with parameterized define-tp layer."
|
|
(tp-test-with-temp-buffer
|
|
(define-tp tp-width (pixels)
|
|
`(display (space :width (,pixels))))
|
|
;; String form with parameterized layer
|
|
(let ((result (tp-put-layer "emacs" '(tp-width 10) 0)))
|
|
(should (eq (get-text-property 0 'tp-name result) 'tp-width))
|
|
(should (equal (get-text-property 0 'display result) '(space :width (10)))))))
|
|
|
|
;; Tests for reactive variables with define-tp layers
|
|
(ert-deftest tp-test-define-tp-with-reactive-var-needs-tp-name ()
|
|
"Test define-tp layers mixed with reactive variables get anonymous tp-name."
|
|
(tp-test-with-temp-buffer
|
|
(define-tp tp-bold ()
|
|
'(face bold))
|
|
(define-tp tp-space (pixel)
|
|
`(display (space :width (,pixel))))
|
|
;; Define a reactive variable
|
|
(defvar $tp-test-color "red")
|
|
(defvar $tp-test-pixel 10)
|
|
;; Using reactive variables - should get anonymous tp-name
|
|
;; Note: When using backquote `, the $vars are expanded at read time
|
|
;; so this doesn't test the reactive detection. Instead we test that
|
|
;; the expansion works correctly.
|
|
(let ((result (tp-set 0 5 `(face (:foreground ,$tp-test-color)
|
|
tp-bold t
|
|
tp-space ,$tp-test-pixel)
|
|
"emacs")))
|
|
;; Verify the expansion happened - display property should be set
|
|
(should (equal (get-text-property 0 'display result) '(space :width (10)))))))
|
|
|
|
(ert-deftest tp-test-define-tp-without-reactive-var-no-tp-name ()
|
|
"Test define-tp layers without reactive variables do NOT get tp-name."
|
|
(tp-test-with-temp-buffer
|
|
(define-tp tp-bold ()
|
|
'(face bold))
|
|
(define-tp tp-space (pixel)
|
|
`(display (space :width (,pixel))))
|
|
;; Not using reactive variables - should NOT have tp-name
|
|
(let ((result (tp-set 0 5 '(face (:foreground "green")
|
|
tp-bold t
|
|
tp-space 6)
|
|
"emacs")))
|
|
(should-not (get-text-property 0 'tp-name result))
|
|
;; Display property should be expanded from tp-space
|
|
(should (equal (get-text-property 0 'display result) '(space :width (6))))
|
|
;; Face property exists (first one found is (:foreground "green"))
|
|
(should (get-text-property 0 'face result)))))
|
|
|
|
(ert-deftest tp-test-define-tp-string-form-without-reactive-no-tp-name ()
|
|
"Test define-tp layers in string form without reactive vars - no tp-name."
|
|
(tp-test-with-temp-buffer
|
|
(define-tp tp-bold ()
|
|
'(face bold))
|
|
(define-tp tp-space (pixel)
|
|
`(display (space :width (,pixel))))
|
|
;; String form - not using reactive variables - should NOT have tp-name
|
|
(let ((result (tp-set "emacs"
|
|
'face '(:foreground "green")
|
|
'tp-bold t
|
|
'tp-space 6)))
|
|
(should-not (get-text-property 0 'tp-name result))
|
|
;; Display property should be expanded from tp-space
|
|
(should (equal (get-text-property 0 'display result) '(space :width (6))))
|
|
;; Face property exists
|
|
(should (get-text-property 0 'face result)))))
|
|
|
|
;;; ============================================================
|
|
;;; Batched Updates Tests
|
|
;;; ============================================================
|
|
|
|
(ert-deftest tp-test-batch-updates-basic ()
|
|
"Test that tp-with-batch-updates defers reactive updates."
|
|
(tp-test-with-temp-buffer
|
|
(unwind-protect
|
|
(progn
|
|
;; Define a reactive layer
|
|
(define-tp test-batch-layer ()
|
|
:props '(face (:foreground $tp-test-batch-color))
|
|
:data '((tp-test-batch-color . "red")))
|
|
(insert "Hello World")
|
|
(tp-set 1 6 'test-batch-layer)
|
|
;; Initial color should be red
|
|
(should (equal (plist-get (tp-at 1 'face) :foreground) "red"))
|
|
;; Now use batch updates
|
|
(tp-with-batch-updates
|
|
(setq tp-test-batch-color "blue")
|
|
;; Inside batch, layer definition is updated but buffer may not be
|
|
;; (implementation note: the layer props are always updated immediately)
|
|
)
|
|
;; After batch ends, buffer should be updated
|
|
(should (equal (plist-get (tp-at 1 'face) :foreground) "blue")))
|
|
;; Cleanup
|
|
(ignore-errors (makunbound 'tp-test-batch-color)))))
|
|
|
|
(ert-deftest tp-test-batch-updates-multiple-vars ()
|
|
"Test that tp-with-batch-updates consolidates multiple variable changes."
|
|
(tp-test-with-temp-buffer
|
|
(unwind-protect
|
|
(progn
|
|
;; Define a reactive layer with multiple vars
|
|
(define-tp test-multi-batch ()
|
|
:props '(face (:foreground $tp-test-fg :background $tp-test-bg))
|
|
:data '((tp-test-fg . "white") (tp-test-bg . "black")))
|
|
(insert "Hello World")
|
|
(tp-set 1 6 'test-multi-batch)
|
|
;; Use batch updates
|
|
(tp-with-batch-updates
|
|
(setq tp-test-fg "yellow")
|
|
(setq tp-test-bg "navy"))
|
|
;; Both should be updated
|
|
(should (equal (plist-get (tp-at 1 'face) :foreground) "yellow"))
|
|
(should (equal (plist-get (tp-at 1 'face) :background) "navy")))
|
|
;; Cleanup
|
|
(ignore-errors (makunbound 'tp-test-fg))
|
|
(ignore-errors (makunbound 'tp-test-bg)))))
|
|
|
|
;;; ============================================================
|
|
;;; Debug Mode Tests
|
|
;;; ============================================================
|
|
|
|
(ert-deftest tp-test-debug-mode-logs ()
|
|
"Test that debug mode logs to *tp-debug* buffer."
|
|
(tp-test-with-temp-buffer
|
|
(let ((tp-debug-mode t)
|
|
(tp-debug-echo nil))
|
|
;; Clear any existing debug buffer
|
|
(tp-debug-clear)
|
|
;; Log a message
|
|
(tp-debug-log "Test message %d" 42)
|
|
;; Check the debug buffer
|
|
(with-current-buffer (get-buffer "*tp-debug*")
|
|
(should (string-match-p "Test message 42" (buffer-string)))))))
|
|
|
|
(ert-deftest tp-test-debug-mode-disabled ()
|
|
"Test that debug mode does not log when disabled."
|
|
(tp-test-with-temp-buffer
|
|
(let ((tp-debug-mode nil))
|
|
;; Clear any existing debug buffer
|
|
(tp-debug-clear)
|
|
;; Try to log a message
|
|
(tp-debug-log "Should not appear")
|
|
;; Check that buffer is empty or doesn't exist
|
|
(let ((buf (get-buffer "*tp-debug*")))
|
|
(if buf
|
|
(with-current-buffer buf
|
|
(should (string= (buffer-string) ""))))))))
|
|
|
|
;;; ============================================================
|
|
;;; Value Transformation Tests
|
|
;;; ============================================================
|
|
|
|
(ert-deftest tp-test-transform-basic ()
|
|
"Test that :transform transforms tp-text values."
|
|
(tp-test-with-temp-buffer
|
|
(unwind-protect
|
|
(progn
|
|
;; Define a layer with transform
|
|
(define-tp test-transform-layer ()
|
|
:props '(face bold tp-text $tp-test-value)
|
|
:data '((tp-test-value . "hello"))
|
|
:transform #'upcase)
|
|
(insert "placeholder")
|
|
(tp-set 1 12 'test-transform-layer)
|
|
;; Text should be transformed to uppercase
|
|
(should (equal (buffer-substring-no-properties 1 6) "HELLO")))
|
|
;; Cleanup
|
|
(ignore-errors (makunbound 'tp-test-value)))))
|
|
|
|
(ert-deftest tp-test-transform-with-reactive-update ()
|
|
"Test that :transform works with reactive updates."
|
|
(tp-test-with-temp-buffer
|
|
(unwind-protect
|
|
(progn
|
|
;; Define a layer with transform (format as currency)
|
|
(define-tp test-currency-layer ()
|
|
:props '(face bold tp-text $tp-test-amount)
|
|
:data '((tp-test-amount . "100"))
|
|
:transform (lambda (text)
|
|
(format "$%s.00" text)))
|
|
(insert "placeholder")
|
|
(tp-set 1 12 'test-currency-layer)
|
|
;; Text should be formatted
|
|
(should (equal (buffer-substring-no-properties 1 8) "$100.00"))
|
|
;; Update the variable
|
|
(setq tp-test-amount "250")
|
|
;; Text should be updated with transform applied
|
|
(should (equal (buffer-substring-no-properties 1 8) "$250.00")))
|
|
;; Cleanup
|
|
(ignore-errors (makunbound 'tp-test-amount)))))
|
|
|
|
(ert-deftest tp-test-transform-removed-on-redefine ()
|
|
"Test that :transform is removed when layer is redefined without it."
|
|
(tp-test-with-temp-buffer
|
|
(unwind-protect
|
|
(progn
|
|
;; Define with transform
|
|
(define-tp test-redef-transform ()
|
|
:props '(face bold tp-text $tp-test-text)
|
|
:data '((tp-test-text . "hello"))
|
|
:transform #'upcase)
|
|
;; Check transform is registered
|
|
(should (assoc 'test-redef-transform tp-layer-transforms))
|
|
;; Redefine without transform
|
|
(define-tp test-redef-transform ()
|
|
:props '(face bold tp-text $tp-test-text)
|
|
:data '((tp-test-text . "hello")))
|
|
;; Transform should be removed
|
|
(should-not (assoc 'test-redef-transform tp-layer-transforms)))
|
|
;; Cleanup
|
|
(ignore-errors (makunbound 'tp-test-text)))))
|
|
|
|
;;; ============================================================
|
|
;;; Built-in Text Property Name Validation Tests
|
|
;;; ============================================================
|
|
|
|
(ert-deftest tp-test-builtin-text-property-check ()
|
|
"Test that tp--builtin-text-property-p correctly identifies built-in properties."
|
|
;; Check known built-in properties
|
|
(should (tp--builtin-text-property-p 'face))
|
|
(should (tp--builtin-text-property-p 'display))
|
|
(should (tp--builtin-text-property-p 'invisible))
|
|
(should (tp--builtin-text-property-p 'help-echo))
|
|
(should (tp--builtin-text-property-p 'keymap))
|
|
(should (tp--builtin-text-property-p 'mouse-face))
|
|
(should (tp--builtin-text-property-p 'read-only))
|
|
(should (tp--builtin-text-property-p 'front-sticky))
|
|
(should (tp--builtin-text-property-p 'rear-nonsticky))
|
|
;; Check non-built-in properties
|
|
(should-not (tp--builtin-text-property-p 'tp-my-custom-layer))
|
|
(should-not (tp--builtin-text-property-p 'my-layer))
|
|
(should-not (tp--builtin-text-property-p 'custom-property)))
|
|
|
|
(ert-deftest tp-test-define-tp-rejects-builtin-names ()
|
|
"Test that define-tp rejects built-in text property names."
|
|
(tp-test-with-temp-buffer
|
|
;; Test that using 'face as a layer name raises an error
|
|
(should-error
|
|
(eval '(define-tp face () '(help-echo "test"))))
|
|
;; Test that using 'display as a layer name raises an error
|
|
(should-error
|
|
(eval '(define-tp display () '(face bold))))
|
|
;; Test that using 'invisible as a layer name raises an error
|
|
(should-error
|
|
(eval '(define-tp invisible () '(face bold))))
|
|
;; Test that using 'keymap as a layer name raises an error
|
|
(should-error
|
|
(eval '(define-tp keymap () '(face bold))))
|
|
;; Test that valid names work fine
|
|
(should (define-tp tp-test-valid-layer () '(face bold)))))
|
|
|
|
(ert-deftest tp-test-define-tp-parameterized-rejects-builtin ()
|
|
"Test that parameterized define-tp also rejects built-in names."
|
|
(tp-test-with-temp-buffer
|
|
;; Parameterized layer with built-in name should fail
|
|
(should-error
|
|
(eval '(define-tp display (value) `(face (:height ,value)))))
|
|
;; Valid parameterized layer should work
|
|
(should (define-tp tp-test-param-valid (value) `(face (:height ,value))))))
|
|
|
|
;;; ============================================================
|
|
;;; Nested Layer Resolution Tests
|
|
;;; ============================================================
|
|
|
|
(ert-deftest tp-test-nested-layer-resolution ()
|
|
"Test that nested custom layers are resolved to built-in properties.
|
|
When a layer's body returns a plist containing other custom layer names,
|
|
those should be recursively expanded to their built-in properties."
|
|
(tp-test-with-temp-buffer
|
|
;; Define a base layer that returns built-in properties
|
|
(define-tp tp-test-base-layer (color)
|
|
`(face (:foreground ,color :background "white")))
|
|
;; Define a wrapper layer that uses the base layer
|
|
(define-tp tp-test-wrapper-layer (plist)
|
|
(let ((color (plist-get plist :color)))
|
|
`(tp-test-base-layer ,color
|
|
help-echo "wrapper")))
|
|
;; Use the wrapper layer
|
|
(let ((result (tp-set "test" 'tp-test-wrapper-layer '(:color "red"))))
|
|
;; The face property should be resolved from tp-test-base-layer
|
|
(should (equal (plist-get (get-text-property 0 'face result) :foreground) "red"))
|
|
(should (equal (plist-get (get-text-property 0 'face result) :background) "white"))
|
|
;; help-echo should also be present
|
|
(should (equal (get-text-property 0 'help-echo result) "wrapper"))
|
|
;; tp-test-base-layer should NOT be present as a property
|
|
(should (null (get-text-property 0 'tp-test-base-layer result))))))
|
|
|
|
(ert-deftest tp-test-deeply-nested-layer-resolution ()
|
|
"Test that deeply nested layers (3 levels) are fully resolved."
|
|
(tp-test-with-temp-buffer
|
|
;; Define 3 levels of nesting
|
|
(define-tp tp-test-level1 (val)
|
|
`(face (:foreground ,val)))
|
|
(define-tp tp-test-level2 (val)
|
|
`(tp-test-level1 ,val help-echo "level2"))
|
|
(define-tp tp-test-level3 (val)
|
|
`(tp-test-level2 ,val display "level3"))
|
|
;; Use the most deeply nested layer
|
|
(let ((result (tp-set "test" 'tp-test-level3 "blue")))
|
|
;; All properties should be resolved
|
|
(should (equal (plist-get (get-text-property 0 'face result) :foreground) "blue"))
|
|
(should (equal (get-text-property 0 'help-echo result) "level2"))
|
|
(should (equal (get-text-property 0 'display result) "level3"))
|
|
;; None of the custom layer names should be present
|
|
(should (null (get-text-property 0 'tp-test-level1 result)))
|
|
(should (null (get-text-property 0 'tp-test-level2 result))))))
|
|
|
|
;;; ============================================================
|
|
;;; Duplicate Property Merging Tests
|
|
;;; ============================================================
|
|
|
|
(ert-deftest tp-test-merge-duplicate-face-symbols ()
|
|
"Test that multiple face symbols in one call are merged into a face list."
|
|
(tp-test-with-temp-buffer
|
|
(let ((result (tp-set "emacs"
|
|
'face 'bold
|
|
'face 'italic)))
|
|
;; Should be a list with italic first (later takes precedence)
|
|
(let ((face-prop (get-text-property 0 'face result)))
|
|
(should (listp face-prop))
|
|
(should (memq 'bold face-prop))
|
|
(should (memq 'italic face-prop))
|
|
;; italic should come before bold (later value takes precedence)
|
|
(should (< (cl-position 'italic face-prop)
|
|
(cl-position 'bold face-prop)))))))
|
|
|
|
(ert-deftest tp-test-merge-duplicate-face-plists ()
|
|
"Test that multiple face plists in one call are merged."
|
|
(tp-test-with-temp-buffer
|
|
(let ((result (tp-set "emacs"
|
|
'face '(:background "green")
|
|
'face '(:foreground "red"))))
|
|
(let ((face-prop (get-text-property 0 'face result)))
|
|
;; Should be a merged plist
|
|
(should (plist-get face-prop :background))
|
|
(should (plist-get face-prop :foreground))
|
|
(should (equal (plist-get face-prop :background) "green"))
|
|
(should (equal (plist-get face-prop :foreground) "red"))))))
|
|
|
|
(ert-deftest tp-test-merge-duplicate-face-later-overrides ()
|
|
"Test that later face plist values override earlier ones for same key."
|
|
(tp-test-with-temp-buffer
|
|
(let ((result (tp-set "emacs"
|
|
'face '(:foreground "red")
|
|
'face '(:foreground "yellow"))))
|
|
(let ((face-prop (get-text-property 0 'face result)))
|
|
;; Later value should override
|
|
(should (equal (plist-get face-prop :foreground) "yellow"))))))
|
|
|
|
(ert-deftest tp-test-merge-other-props-later-overrides ()
|
|
"Test that non-face duplicate properties use later value."
|
|
(tp-test-with-temp-buffer
|
|
(let ((result (tp-set "emacs"
|
|
'help-echo "first"
|
|
'help-echo "second")))
|
|
(should (equal (get-text-property 0 'help-echo result) "second")))))
|
|
|
|
(ert-deftest tp-test-merge-multiple-layers-with-face ()
|
|
"Test merging multiple layers that each contribute face properties."
|
|
(tp-test-with-temp-buffer
|
|
(define-tp tp-test-layer1 ()
|
|
'(face (:foreground "blue")))
|
|
(define-tp tp-test-layer2 ()
|
|
'(face (:background "yellow")))
|
|
(let ((result (tp-set "emacs"
|
|
'tp-test-layer1 t
|
|
'tp-test-layer2 t
|
|
'face '(:weight bold))))
|
|
(let ((face-prop (get-text-property 0 'face result)))
|
|
;; All face properties should be merged
|
|
(should (equal (plist-get face-prop :foreground) "blue"))
|
|
(should (equal (plist-get face-prop :background) "yellow"))
|
|
(should (equal (plist-get face-prop :weight) 'bold))))))
|
|
|
|
(ert-deftest tp-test-merge-in-region-form ()
|
|
"Test duplicate property merging in region form."
|
|
(tp-test-with-temp-buffer
|
|
(insert "Hello World")
|
|
(tp-set 1 6 '(face bold face (:foreground "green")))
|
|
(let ((face-prop (tp-at 1 'face)))
|
|
;; Should be a list with plist and symbol
|
|
(should (listp face-prop))
|
|
;; Check properties
|
|
(should (or (memq 'bold face-prop)
|
|
(eq face-prop 'bold))))))
|
|
|
|
(ert-deftest tp-test-tp-add-merge-faces ()
|
|
"Test that tp-add also merges duplicate face properties."
|
|
(tp-test-with-temp-buffer
|
|
(insert "Hello World")
|
|
(tp-add 1 6 '(face bold face (:foreground "red")))
|
|
(let ((face-prop (tp-at 1 'face)))
|
|
;; Should have both face values merged
|
|
(should (listp face-prop))
|
|
(should (memq 'bold face-prop))
|
|
;; Check for the plist part with :foreground
|
|
(should (cl-some (lambda (f)
|
|
(and (listp f)
|
|
(keywordp (car f))
|
|
(equal (plist-get f :foreground) "red")))
|
|
face-prop)))))
|
|
|
|
(ert-deftest tp-test-tp-reset-merge-faces ()
|
|
"Test that tp-reset also merges duplicate face properties."
|
|
(tp-test-with-temp-buffer
|
|
(insert "Hello World")
|
|
(tp-reset 1 6 '(face bold face (:foreground "red")))
|
|
(let ((face-prop (tp-at 1 'face)))
|
|
;; Should have both face values merged
|
|
(should (listp face-prop))
|
|
(should (memq 'bold face-prop)))))
|
|
|
|
(ert-deftest tp-test-merge-mouse-face ()
|
|
"Test that mouse-face properties are also merged."
|
|
(tp-test-with-temp-buffer
|
|
(let ((result (tp-set "emacs"
|
|
'mouse-face 'highlight
|
|
'mouse-face '(:background "blue"))))
|
|
(let ((mouse-face-prop (get-text-property 0 'mouse-face result)))
|
|
;; Should be a list with plist and symbol
|
|
(should (listp mouse-face-prop))
|
|
(should (memq 'highlight mouse-face-prop))))))
|
|
|
|
;;; ============================================================
|
|
;;; Nil Value Property Tests (Issue: "Odd length text property list")
|
|
;;; ============================================================
|
|
|
|
(ert-deftest tp-test-set-with-nil-value ()
|
|
"Test tp-set with nil value produces valid property list.
|
|
Regression test for: (tp-set \"emacs\" 'face nil) erroring with
|
|
\"Odd length text property list\"."
|
|
(tp-test-with-temp-buffer
|
|
(let ((result (tp-set "emacs" 'face nil)))
|
|
;; Result should be #("emacs" 0 5 (face nil))
|
|
(should (stringp result))
|
|
(should (eq (get-text-property 0 'face result) nil))
|
|
;; Verify the property list is valid (has even length)
|
|
(let ((props (text-properties-at 0 result)))
|
|
(should (= (% (length props) 2) 0))))))
|
|
|
|
(ert-deftest tp-test-set-with-nil-value-in-middle ()
|
|
"Test tp-set with nil value in middle of property list."
|
|
(tp-test-with-temp-buffer
|
|
(let ((result (tp-set "emacs" 'face 'bold 'help-echo nil 'display "test")))
|
|
;; Result should have face=bold, help-echo=nil, display="test"
|
|
(should (eq (get-text-property 0 'face result) 'bold))
|
|
(should (eq (get-text-property 0 'help-echo result) nil))
|
|
(should (equal (get-text-property 0 'display result) "test")))))
|
|
|
|
(ert-deftest tp-test-set-with-multiple-nil-values ()
|
|
"Test tp-set with multiple nil values."
|
|
(tp-test-with-temp-buffer
|
|
(let ((result (tp-set "emacs" 'face nil 'help-echo nil)))
|
|
(should (eq (get-text-property 0 'face result) nil))
|
|
(should (eq (get-text-property 0 'help-echo result) nil)))))
|
|
|
|
(ert-deftest tp-test-reset-with-nil-value ()
|
|
"Test tp-reset with nil value works correctly."
|
|
(tp-test-with-temp-buffer
|
|
(let ((result (tp-reset "emacs" 'face nil)))
|
|
;; Result should have face=nil
|
|
(should (eq (get-text-property 0 'face result) nil)))))
|
|
|
|
(ert-deftest tp-test-add-with-nil-value ()
|
|
"Test tp-add with nil value works correctly."
|
|
(tp-test-with-temp-buffer
|
|
(let ((result (tp-add "emacs" 'face nil)))
|
|
;; Result should have face=nil
|
|
(should (eq (get-text-property 0 'face result) nil)))))
|
|
|
|
(ert-deftest tp-test-set-nil-value-in-buffer ()
|
|
"Test tp-set with nil value in buffer region."
|
|
(tp-test-with-temp-buffer
|
|
(insert "emacs")
|
|
(tp-set 1 6 '(face nil))
|
|
(should (eq (tp-at 1 'face) nil))))
|
|
|
|
;;; ============================================================
|
|
;;; Non-Destructive String Modification Tests
|
|
;;; ============================================================
|
|
|
|
(ert-deftest tp-test-set-preserves-text-property-intervals ()
|
|
"Test that tp-set preserves text property intervals when adding new properties.
|
|
When a string has different properties at different positions, adding a new
|
|
property should preserve the original interval structure.
|
|
|
|
Test string: \" button \" (8 characters, positions 0-7)
|
|
- Position 0-1: display property (first space character)
|
|
- Position 1-7: no display property (text \"button \")
|
|
- Position 7-8: display property (last space character)"
|
|
(let ((original #(" button " 0 1 (display (space :width (4)))
|
|
7 8 (display (space :width (4))))))
|
|
(let ((result (tp-set original 'face '(:foreground "red"))))
|
|
;; Result should be a new string with properties
|
|
(should (stringp result))
|
|
;; Result should NOT be the same object as original
|
|
(should (not (eq original result)))
|
|
;; Original should NOT be modified
|
|
(should (null (get-text-property 0 'face original)))
|
|
;; Result should have face property everywhere
|
|
(should (equal (get-text-property 0 'face result) '(:foreground "red")))
|
|
(should (equal (get-text-property 4 'face result) '(:foreground "red")))
|
|
(should (equal (get-text-property 7 'face result) '(:foreground "red")))
|
|
;; Result should preserve display property at original positions
|
|
(should (equal (get-text-property 0 'display result) '(space :width (4))))
|
|
(should (null (get-text-property 2 'display result))) ;; No display at position 2
|
|
(should (equal (get-text-property 7 'display result) '(space :width (4)))))))
|
|
|
|
(ert-deftest tp-test-set-does-not-modify-original-string ()
|
|
"Test that tp-set returns a new string and does not modify the original."
|
|
(let ((original "Hello"))
|
|
(let ((result (tp-set original 'face 'bold)))
|
|
;; Result should be a new string with properties
|
|
(should (stringp result))
|
|
(should (eq (get-text-property 0 'face result) 'bold))
|
|
;; Original should NOT be modified (no properties)
|
|
(should (null (get-text-property 0 'face original)))
|
|
;; Strings should not be eq (different objects)
|
|
(should (not (eq original result))))))
|
|
|
|
(ert-deftest tp-test-reset-does-not-modify-original-string ()
|
|
"Test that tp-reset returns a new string and does not modify the original."
|
|
(let ((original "Hello"))
|
|
(let ((result (tp-reset original 'face 'bold)))
|
|
;; Result should be a new string with properties
|
|
(should (stringp result))
|
|
(should (eq (get-text-property 0 'face result) 'bold))
|
|
;; Original should NOT be modified (no properties)
|
|
(should (null (get-text-property 0 'face original)))
|
|
;; Strings should not be eq (different objects)
|
|
(should (not (eq original result))))))
|
|
|
|
(ert-deftest tp-test-add-does-not-modify-original-string ()
|
|
"Test that tp-add returns a new string and does not modify the original."
|
|
(let ((original "Hello"))
|
|
(let ((result (tp-add original 'face 'bold)))
|
|
;; Result should be a new string with properties
|
|
(should (stringp result))
|
|
(should (eq (get-text-property 0 'face result) 'bold))
|
|
;; Original should NOT be modified (no properties)
|
|
(should (null (get-text-property 0 'face original)))
|
|
;; Strings should not be eq (different objects)
|
|
(should (not (eq original result))))))
|
|
|
|
(ert-deftest tp-test-remove-does-not-modify-original-string ()
|
|
"Test that tp-remove returns a new string and does not modify the original."
|
|
;; First create a propertized string (using propertize to create the original)
|
|
(let ((original (propertize "Hello" 'face 'bold 'help-echo "tip")))
|
|
(let ((result (tp-remove original 'face)))
|
|
;; Result should be a new string without face property
|
|
(should (stringp result))
|
|
(should (null (get-text-property 0 'face result)))
|
|
(should (equal (get-text-property 0 'help-echo result) "tip"))
|
|
;; Original should still have face property
|
|
(should (eq (get-text-property 0 'face original) 'bold))
|
|
;; Strings should not be eq (different objects)
|
|
(should (not (eq original result))))))
|
|
|
|
(ert-deftest tp-test-set-region-modifies-original-string ()
|
|
"Test that tp-set with region form DOES modify the original string.
|
|
The region form (tp-set START END PROPS STRING) modifies the string in-place."
|
|
(let ((original (copy-sequence "Hello World")))
|
|
(let ((result (tp-set 0 5 '(face bold) original)))
|
|
;; Result should be the same object as original (modified in-place)
|
|
(should (eq result original))
|
|
;; Both should have the face property
|
|
(should (eq (get-text-property 0 'face result) 'bold))
|
|
(should (eq (get-text-property 0 'face original) 'bold)))))
|
|
|
|
(ert-deftest tp-test-match-set-does-not-modify-original-string ()
|
|
"Test that tp-match-set returns a new string and does not modify the original."
|
|
(let ((original "Hello World"))
|
|
(let ((result (tp-match-set "Hello" '(face bold) original)))
|
|
;; Result should be a new string with properties
|
|
(should (stringp result))
|
|
(should (eq (get-text-property 0 'face result) 'bold))
|
|
;; Original should NOT be modified (no properties)
|
|
(should (null (get-text-property 0 'face original)))
|
|
;; Strings should not be eq (different objects)
|
|
(should (not (eq original result))))))
|
|
|
|
(ert-deftest tp-test-regexp-set-does-not-modify-original-string ()
|
|
"Test that tp-regexp-set returns a new string and does not modify the original."
|
|
(let ((original "abc 123 def"))
|
|
(let ((result (tp-regexp-set "[0-9]+" '(face bold) original)))
|
|
;; Result should be a new string with properties on the match
|
|
(should (stringp result))
|
|
(should (eq (get-text-property 4 'face result) 'bold))
|
|
;; Original should NOT be modified (no properties)
|
|
(should (null (get-text-property 4 'face original)))
|
|
;; Strings should not be eq (different objects)
|
|
(should (not (eq original result))))))
|
|
|
|
(ert-deftest tp-test-remove-custom-layer ()
|
|
"Test that tp-remove correctly removes custom text property layers.
|
|
When a layer is removed, only its face contribution should be removed,
|
|
not the entire face property."
|
|
;; First define the custom layer
|
|
(tp-layer-reset)
|
|
(eval '(define-tp tp-delete (color)
|
|
`(face (:strike-through ,color))))
|
|
;; Test with entire string form
|
|
(let* ((str "emacs")
|
|
(str-with-props (tp-set str 'face 'bold 'tp-delete t))
|
|
(result (tp-remove str-with-props 'tp-delete)))
|
|
;; Original should still have the properties
|
|
(should (get-text-property 0 'face str-with-props))
|
|
;; Result should have face 'bold (only the tp-delete contribution removed)
|
|
(should (equal (get-text-property 0 'face result) 'bold))
|
|
;; Result should not have tp-delete property
|
|
(should (null (get-text-property 0 'tp-delete result)))
|
|
;; Result should not have tp-name property
|
|
(should (null (get-text-property 0 'tp-name result)))))
|
|
|
|
(ert-deftest tp-test-remove-custom-layer-preserves-other-props ()
|
|
"Test that tp-remove with layer name preserves other properties.
|
|
When the layer property is set (via mixed syntax), its face contribution
|
|
can be tracked and removed."
|
|
(tp-layer-reset)
|
|
(eval '(define-tp tp-delete (color)
|
|
`(face (:strike-through ,color))))
|
|
;; Test with mixed syntax - set layer alongside other properties
|
|
;; This allows tracking of the layer property
|
|
(let* ((str "emacs")
|
|
(str-with-props (tp-set str 'help-echo "test" 'tp-delete "red"))
|
|
(result (tp-remove str-with-props 'tp-delete)))
|
|
;; help-echo should still be present
|
|
(should (equal (get-text-property 0 'help-echo result) "test"))
|
|
;; face (from tp-delete) should be removed
|
|
(should (null (get-text-property 0 'face result)))
|
|
;; tp-delete property should be removed
|
|
(should (null (get-text-property 0 'tp-delete result)))))
|
|
|
|
(provide 'tp-tests)
|
|
;;; tp-tests.el ends here
|