summaryrefslogtreecommitdiff
path: root/tests
diff options
context:
space:
mode:
Diffstat (limited to 'tests')
-rw-r--r--tests/test-browser-config.el277
-rw-r--r--tests/test-clear-blank-lines.el.disabled47
-rw-r--r--tests/test-custom-comments-comment-block-banner.el228
-rw-r--r--tests/test-custom-comments-comment-box.el241
-rw-r--r--tests/test-custom-comments-comment-heavy-box.el251
-rw-r--r--tests/test-custom-comments-comment-inline-border.el235
-rw-r--r--tests/test-custom-comments-comment-padded-divider.el250
-rw-r--r--tests/test-custom-comments-comment-reformat.el191
-rw-r--r--tests/test-custom-comments-comment-simple-divider.el246
-rw-r--r--tests/test-custom-comments-comment-unicode-box.el264
-rw-r--r--tests/test-custom-comments-delete-buffer-comments.el224
-rw-r--r--tests/test-custom-file-buffer-clear-to-bottom-of-buffer.el163
-rw-r--r--tests/test-custom-file-buffer-clear-to-top-of-buffer.el162
-rw-r--r--tests/test-custom-file-buffer-copy-link-to-buffer-file.el209
-rw-r--r--tests/test-custom-file-buffer-copy-path-to-buffer-file-as-kill.el205
-rw-r--r--tests/test-custom-file-buffer-copy-whole-buffer.el194
-rw-r--r--tests/test-custom-file-buffer-delete-buffer-and-file.el671
-rw-r--r--tests/test-custom-file-buffer-move-buffer-and-file.el936
-rw-r--r--tests/test-custom-file-buffer-rename-buffer-and-file.el939
-rw-r--r--tests/test-custom-misc-count-words.el148
-rw-r--r--tests/test-custom-misc-format-region.el161
-rw-r--r--tests/test-custom-misc-jump-to-matching-paren.el197
-rw-r--r--tests/test-custom-misc-replace-fraction-glyphs.el185
-rw-r--r--tests/test-custom-ordering-alphabetize.el176
-rw-r--r--tests/test-custom-ordering-arrayify.el215
-rw-r--r--tests/test-custom-ordering-comma-to-lines.el159
-rw-r--r--tests/test-custom-ordering-number-lines.el181
-rw-r--r--tests/test-custom-ordering-reverse-lines.el131
-rw-r--r--tests/test-custom-ordering-toggle-quotes.el155
-rw-r--r--tests/test-custom-ordering-unarrayify.el159
-rw-r--r--tests/test-custom-org-agenda-functions.el.disabled94
-rw-r--r--tests/test-custom-text-enclose-append.el190
-rw-r--r--tests/test-custom-text-enclose-indent.el241
-rw-r--r--tests/test-custom-text-enclose-prepend.el207
-rw-r--r--tests/test-custom-text-enclose-surround.el200
-rw-r--r--tests/test-custom-text-enclose-unwrap.el266
-rw-r--r--tests/test-custom-text-enclose-wrap.el240
-rw-r--r--tests/test-custom-whitespace-collapse.el150
-rw-r--r--tests/test-custom-whitespace-delete-all.el150
-rw-r--r--tests/test-custom-whitespace-delete-blank-lines.el146
-rw-r--r--tests/test-custom-whitespace-ensure-single-blank.el146
-rw-r--r--tests/test-custom-whitespace-hyphenate.el140
-rw-r--r--tests/test-custom-whitespace-remove-leading-trailing.el157
-rw-r--r--tests/test-fixup-whitespace.el.disabled159
-rw-r--r--tests/test-flyspell-config-functions.el.disabled149
-rw-r--r--tests/test-format-region.el.disabled110
-rw-r--r--tests/test-jumper.el352
-rw-r--r--tests/test-lorem-optimum-benchmark.el227
-rw-r--r--tests/test-lorem-optimum.el242
-rw-r--r--tests/test-org-contacts-parse-email.el219
-rw-r--r--tests/test-org-roam-config-demote.el183
-rw-r--r--tests/test-org-roam-config-format.el151
-rw-r--r--tests/test-org-roam-config-link-description.el188
-rw-r--r--tests/test-org-roam-config-slug.el223
-rw-r--r--tests/test-org-webclipper-process.el210
-rw-r--r--tests/test-test-runner.el359
-rw-r--r--tests/test-theme-theme-persistence.el.disabled135
-rw-r--r--tests/test-title-case-region.el.disabled44
-rw-r--r--tests/test-undead-buffers.el11
-rw-r--r--tests/testutil-general.el25
60 files changed, 12350 insertions, 764 deletions
diff --git a/tests/test-browser-config.el b/tests/test-browser-config.el
new file mode 100644
index 00000000..6ab756dd
--- /dev/null
+++ b/tests/test-browser-config.el
@@ -0,0 +1,277 @@
+;;; test-browser-config.el --- Tests for browser-config.el -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; Unit tests for browser-config.el - browser selection and configuration.
+;;
+;; Testing approach:
+;; - Tests focus on internal `cj/--do-*` functions (pure business logic)
+;; - File I/O tests use temp files
+;; - executable-find is stubbed to control available browsers
+;; - Each test is isolated with setup/teardown
+;; - Tests verify return values, not user messages
+
+;;; Code:
+
+(require 'ert)
+(require 'testutil-general)
+
+;; Add modules directory to load path
+(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory))
+
+;; Load the module with temp file to avoid polluting real config
+(defvar test-browser--temp-choice-file nil
+ "Temporary file for browser choice during tests.")
+
+(defun test-browser-setup ()
+ "Setup test environment before each test."
+ (setq test-browser--temp-choice-file (make-temp-file "browser-choice-test" nil ".el"))
+ (setq cj/browser-choice-file test-browser--temp-choice-file))
+
+(defun test-browser-teardown ()
+ "Clean up test environment after each test."
+ (when (and test-browser--temp-choice-file
+ (file-exists-p test-browser--temp-choice-file))
+ (delete-file test-browser--temp-choice-file))
+ (setq test-browser--temp-choice-file nil))
+
+;; Now require the module
+(require 'browser-config)
+
+;;; Helper Functions
+
+(defun test-browser-make-plist (name &optional executable path)
+ "Create a test browser plist with NAME, EXECUTABLE, and PATH."
+ (list :function 'eww-browse-url
+ :name name
+ :executable executable
+ :path path
+ :program-var nil))
+
+;;; Normal Cases - Discover Browsers
+
+(ert-deftest test-browser-discover-finds-eww ()
+ "Should always find built-in EWW browser."
+ (test-browser-setup)
+ (let ((browsers (cj/discover-browsers)))
+ (should (cl-find-if (lambda (b) (string= (plist-get b :name) "EWW (Emacs Browser)"))
+ browsers)))
+ (test-browser-teardown))
+
+(ert-deftest test-browser-discover-deduplicates-names ()
+ "Should not return duplicate browser names."
+ (test-browser-setup)
+ (let ((browsers (cj/discover-browsers))
+ (names (mapcar (lambda (b) (plist-get b :name)) (cj/discover-browsers))))
+ (should (= (length names) (length (cl-remove-duplicates names :test 'string=)))))
+ (test-browser-teardown))
+
+;;; Normal Cases - Apply Browser Choice
+
+(ert-deftest test-browser-apply-valid-browser ()
+ "Should successfully apply a valid browser configuration."
+ (test-browser-setup)
+ (let ((browser (test-browser-make-plist "Test Browser")))
+ (let ((result (cj/--do-apply-browser-choice browser)))
+ (should (eq result 'success))
+ (should (eq browse-url-browser-function 'eww-browse-url))))
+ (test-browser-teardown))
+
+(ert-deftest test-browser-apply-sets-program-var ()
+ "Should set browser program variable if specified."
+ (test-browser-setup)
+ (let ((browser (list :function 'browse-url-chrome
+ :name "Chrome"
+ :executable "chrome"
+ :path "/usr/bin/chrome"
+ :program-var 'browse-url-chrome-program)))
+ (cj/--do-apply-browser-choice browser)
+ (should (string= browse-url-chrome-program "/usr/bin/chrome")))
+ (test-browser-teardown))
+
+;;; Normal Cases - Save and Load
+
+(ert-deftest test-browser-save-and-load-choice ()
+ "Should save and load browser choice correctly."
+ (test-browser-setup)
+ (let ((browser (test-browser-make-plist "Saved Browser" "firefox" "/usr/bin/firefox")))
+ (cj/save-browser-choice browser)
+ (let ((loaded (cj/load-browser-choice)))
+ (should loaded)
+ (should (string= (plist-get loaded :name) "Saved Browser"))
+ (should (string= (plist-get loaded :executable) "firefox"))))
+ (test-browser-teardown))
+
+;;; Normal Cases - Choose Browser
+
+(ert-deftest test-browser-choose-saves-and-applies ()
+ "Should save and apply browser choice."
+ (test-browser-setup)
+ (let ((browser (test-browser-make-plist "Test")))
+ (let ((result (cj/--do-choose-browser browser)))
+ (should (eq result 'success))
+ ;; Verify it was saved
+ (let ((loaded (cj/load-browser-choice)))
+ (should (string= (plist-get loaded :name) "Test")))))
+ (test-browser-teardown))
+
+;;; Normal Cases - Initialize Browser
+
+(ert-deftest test-browser-initialize-with-saved-choice ()
+ "Should load and use saved browser choice."
+ (test-browser-setup)
+ (let ((browser (test-browser-make-plist "Saved")))
+ (cj/save-browser-choice browser)
+ (let ((result (cj/--do-initialize-browser)))
+ (should (eq (car result) 'loaded))
+ (should (plist-get (cdr result) :name))
+ (should (string= (plist-get (cdr result) :name) "Saved"))))
+ (test-browser-teardown))
+
+(ert-deftest test-browser-initialize-without-saved-choice ()
+ "Should use first available browser when no saved choice."
+ (test-browser-setup)
+ ;; Delete any saved choice
+ (when (file-exists-p cj/browser-choice-file)
+ (delete-file cj/browser-choice-file))
+ (let ((result (cj/--do-initialize-browser)))
+ (should (eq (car result) 'first-available))
+ (should (plist-get (cdr result) :name)))
+ (test-browser-teardown))
+
+;;; Boundary Cases - Apply Browser
+
+(ert-deftest test-browser-apply-nil-plist ()
+ "Should return 'invalid-plist for nil browser."
+ (test-browser-setup)
+ (let ((result (cj/--do-apply-browser-choice nil)))
+ (should (eq result 'invalid-plist)))
+ (test-browser-teardown))
+
+(ert-deftest test-browser-apply-missing-function ()
+ "Should return 'invalid-plist when :function is missing."
+ (test-browser-setup)
+ (let ((browser (list :name "Bad Browser" :function nil)))
+ (let ((result (cj/--do-apply-browser-choice browser)))
+ (should (eq result 'invalid-plist))))
+ (test-browser-teardown))
+
+(ert-deftest test-browser-apply-with-nil-path ()
+ "Should handle nil path for built-in browser."
+ (test-browser-setup)
+ (let ((browser (test-browser-make-plist "EWW" nil nil)))
+ (let ((result (cj/--do-apply-browser-choice browser)))
+ (should (eq result 'success))))
+ (test-browser-teardown))
+
+;;; Boundary Cases - Save and Load
+
+(ert-deftest test-browser-load-nonexistent-file ()
+ "Should return nil when loading from nonexistent file."
+ (test-browser-setup)
+ (when (file-exists-p cj/browser-choice-file)
+ (delete-file cj/browser-choice-file))
+ (let ((result (cj/load-browser-choice)))
+ (should (null result)))
+ (test-browser-teardown))
+
+(ert-deftest test-browser-load-corrupt-file ()
+ "Should return nil when loading corrupt file."
+ (test-browser-setup)
+ (with-temp-file cj/browser-choice-file
+ (insert "this is not valid elisp {{{"))
+ (let ((result (cj/load-browser-choice)))
+ (should (null result)))
+ (test-browser-teardown))
+
+(ert-deftest test-browser-load-file-without-variable ()
+ "Should return nil when file doesn't define expected variable."
+ (test-browser-setup)
+ (with-temp-file cj/browser-choice-file
+ (insert "(setq some-other-variable 'foo)"))
+ ;; Unset any previously loaded variable
+ (makunbound 'cj/saved-browser-choice)
+ (let ((result (cj/load-browser-choice)))
+ (should (null result)))
+ (test-browser-teardown))
+
+;;; Boundary Cases - Choose Browser
+
+(ert-deftest test-browser-choose-empty-plist ()
+ "Should handle empty plist gracefully."
+ (test-browser-setup)
+ (let ((result (cj/--do-choose-browser nil)))
+ (should (eq result 'invalid-plist)))
+ (test-browser-teardown))
+
+;;; Error Cases - File Operations
+
+(ert-deftest test-browser-save-to-readonly-location ()
+ "Should return 'save-failed when cannot write file."
+ (test-browser-setup)
+ ;; Make file read-only
+ (with-temp-file cj/browser-choice-file
+ (insert ";; test"))
+ (set-file-modes cj/browser-choice-file #o444)
+ (let ((browser (test-browser-make-plist "Test"))
+ (result nil))
+ (setq result (cj/--do-choose-browser browser))
+ ;; Restore permissions before teardown
+ (set-file-modes cj/browser-choice-file #o644)
+ (should (eq result 'save-failed)))
+ (test-browser-teardown))
+
+;;; Browser Discovery Tests
+
+(ert-deftest test-browser-discover-returns-plists ()
+ "Should return properly formatted browser plists."
+ (test-browser-setup)
+ (let ((browsers (cj/discover-browsers)))
+ (should (> (length browsers) 0))
+ (dolist (browser browsers)
+ (should (plist-member browser :function))
+ (should (plist-member browser :name))
+ (should (plist-member browser :executable))
+ (should (plist-member browser :path))))
+ (test-browser-teardown))
+
+(ert-deftest test-browser-format-location-keys ()
+ "Should have all required keys in browser plist."
+ (test-browser-setup)
+ (let ((browsers (cj/discover-browsers)))
+ (when browsers
+ (let ((browser (car browsers)))
+ (should (plist-get browser :function))
+ (should (plist-get browser :name)))))
+ (test-browser-teardown))
+
+;;; Integration Tests
+
+(ert-deftest test-browser-full-cycle ()
+ "Should handle full save-load-apply cycle."
+ (test-browser-setup)
+ (let ((browser (test-browser-make-plist "Cycle Test" "test-browser" "/usr/bin/test")))
+ ;; Choose (save and apply)
+ (should (eq (cj/--do-choose-browser browser) 'success))
+ ;; Verify it was saved
+ (let ((loaded (cj/load-browser-choice)))
+ (should loaded)
+ (should (string= (plist-get loaded :name) "Cycle Test")))
+ ;; Initialize should load the saved choice
+ (let ((result (cj/--do-initialize-browser)))
+ (should (eq (car result) 'loaded))
+ (should (string= (plist-get (cdr result) :name) "Cycle Test"))))
+ (test-browser-teardown))
+
+(ert-deftest test-browser-overwrite-choice ()
+ "Should overwrite previous browser choice."
+ (test-browser-setup)
+ (let ((browser1 (test-browser-make-plist "First"))
+ (browser2 (test-browser-make-plist "Second")))
+ (cj/--do-choose-browser browser1)
+ (cj/--do-choose-browser browser2)
+ (let ((loaded (cj/load-browser-choice)))
+ (should (string= (plist-get loaded :name) "Second"))))
+ (test-browser-teardown))
+
+(provide 'test-browser-config)
+;;; test-browser-config.el ends here
diff --git a/tests/test-clear-blank-lines.el.disabled b/tests/test-clear-blank-lines.el.disabled
deleted file mode 100644
index 2190aba0..00000000
--- a/tests/test-clear-blank-lines.el.disabled
+++ /dev/null
@@ -1,47 +0,0 @@
-;;; test-clear-blank-lines.el --- -*- lexical-binding: t; -*-
-
-;;; Commentary:
-;;
-
-;;; Code:
-
-(require 'ert)
-(add-to-list 'load-path (concat user-emacs-directory "modules"))
-(require 'custom-functions)
-
-(ert-deftest test-cj/clear-blank-lines-region ()
- (let ((testdata "Some\n\n\n\nText")
- (expected "Some\nText")
- (actual))
- (with-temp-buffer
- (insert testdata)
- (cj/clear-blank-lines (point-min) (point-max))
- (setq actual (buffer-string))
- (message "buffer is:\n'%s'" actual)
- (should (string= actual expected)))))
-
-(ert-deftest test-cj/clear-blank-lines-region-multiple-lines ()
- (let ((testdata "Some\n\n\n\nText")
- (expected "Some\n\n\n\nText")
- (midpoint)
- (actual))
- (with-temp-buffer
- (insert testdata)
- (insert "\n")
- (setq midpoint (point))
- (insert testdata)
- (cj/clear-blank-lines (point-min) midpoint)
- (setq actual (buffer-substring (- (point-max)
- (length testdata)) (point-max)))
- (message "buffer is:\n'%s'" (buffer-string))
- (should (string= actual expected)))))
-
-(ert-deftest test-cj/clear-blank-lines-negative ()
- (with-temp-buffer
- (insert "Some\nText")
- (cj/clear-blank-lines (point-min) (point-max))
- (should (equal (buffer-string) "Some\nText"))))
-
-
-(provide 'test-clear-blank-lines)
-;;; test-clear-blank-lines.el ends here.
diff --git a/tests/test-custom-comments-comment-block-banner.el b/tests/test-custom-comments-comment-block-banner.el
new file mode 100644
index 00000000..6561ebfa
--- /dev/null
+++ b/tests/test-custom-comments-comment-block-banner.el
@@ -0,0 +1,228 @@
+;;; test-custom-comments-comment-block-banner.el --- Tests for cj/comment-block-banner -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; Tests for the cj/comment-block-banner function from custom-comments.el
+;;
+;; This function generates a 3-line block banner comment (JSDoc/Doxygen style):
+;; - Top line: comment-start (e.g., /*) + decoration chars
+;; - Text line: space + decoration char + space + text
+;; - Bottom line: space + decoration chars + comment-end (e.g., */)
+;;
+;; This style is common in C, JavaScript, Java, and other languages that use
+;; block comments.
+;;
+;; We test the NON-INTERACTIVE implementation (cj/--comment-block-banner)
+;; to avoid mocking user prompts. This follows our testing best practice
+;; of separating business logic from UI interaction.
+;;
+;; Cross-Language Testing Strategy:
+;; - Comprehensive testing in C (the primary language for this style)
+;; - Representative testing in JavaScript/Java (similar block comment syntax)
+;; - This style is specifically designed for block comments, so we focus
+;; testing on languages that use /* */ syntax
+;; - See test-custom-comments-delete-buffer-comments.el for detailed rationale
+
+;;; Code:
+
+(require 'ert)
+(require 'testutil-general)
+
+;; Add modules directory to load path
+(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory))
+
+;; Stub dependencies before loading the module
+(defvar cj/custom-keymap (make-sparse-keymap)
+ "Stub keymap for testing.")
+
+;; Now load the actual production module
+(require 'custom-comments)
+
+;;; Test Helpers
+
+(defun test-block-banner-at-column (column-pos comment-start comment-end decoration-char text length)
+ "Test cj/--comment-block-banner at COLUMN-POS indentation.
+Insert spaces to reach COLUMN-POS, then call cj/--comment-block-banner with
+COMMENT-START, COMMENT-END, DECORATION-CHAR, TEXT, and LENGTH.
+Returns the buffer string for assertions."
+ (with-temp-buffer
+ (when (> column-pos 0)
+ (insert (make-string column-pos ?\s)))
+ (cj/--comment-block-banner comment-start comment-end decoration-char text length)
+ (buffer-string)))
+
+;;; C/JavaScript/Java Tests (Block Comment Languages - Comprehensive Coverage)
+
+;;; Normal Cases
+
+(ert-deftest test-block-banner-c-basic ()
+ "Should generate 3-line block banner in C style."
+ (let ((result (test-block-banner-at-column 0 "/*" "*/" "*" "Section Header" 70)))
+ ;; Should have 3 lines
+ (should (= 3 (length (split-string result "\n" t))))
+ ;; First line should start with /*
+ (should (string-match-p "^/\\*\\*" result))
+ ;; Middle line should contain text
+ (should (string-match-p "\\* Section Header" result))
+ ;; Last line should end with */
+ (should (string-match-p "\\*/$" result))))
+
+(ert-deftest test-block-banner-c-custom-decoration ()
+ "Should use custom decoration character."
+ (let ((result (test-block-banner-at-column 0 "/*" "*/" "#" "Header" 70)))
+ (should (string-match-p "/\\*#" result))
+ (should (string-match-p " # Header" result))))
+
+(ert-deftest test-block-banner-c-custom-text ()
+ "Should include custom text in banner."
+ (let ((result (test-block-banner-at-column 0 "/*" "*/" "*" "Custom Text Here" 70)))
+ (should (string-match-p "Custom Text Here" result))))
+
+(ert-deftest test-block-banner-c-empty-text ()
+ "Should handle empty text string."
+ (let ((result (test-block-banner-at-column 0 "/*" "*/" "*" "" 70)))
+ ;; Should still generate 3 lines
+ (should (= 3 (length (split-string result "\n" t))))
+ ;; Should have comment delimiters
+ (should (string-match-p "/\\*" result))
+ (should (string-match-p "\\*/$" result))))
+
+(ert-deftest test-block-banner-c-at-column-0 ()
+ "Should work at column 0."
+ (let ((result (test-block-banner-at-column 0 "/*" "*/" "*" "Header" 70)))
+ ;; First character should be /
+ (should (string-prefix-p "/*" result))))
+
+(ert-deftest test-block-banner-c-indented ()
+ "Should work when indented."
+ (let ((result (test-block-banner-at-column 4 "/*" "*/" "*" "Header" 70)))
+ ;; First line should start with spaces
+ (should (string-prefix-p " /*" result))
+ ;; Other lines should be indented
+ (let ((lines (split-string result "\n" t)))
+ (should (string-prefix-p " " (nth 1 lines))) ; text line has extra space
+ (should (string-prefix-p " " (nth 2 lines)))))) ; bottom line has extra space
+
+(ert-deftest test-block-banner-c-short-text ()
+ "Should handle short text properly."
+ (let ((result (test-block-banner-at-column 0 "/*" "*/" "*" "X" 70)))
+ ;; Should have 3 lines
+ (should (= 3 (length (split-string result "\n" t))))
+ ;; Text should be present
+ (should (string-match-p "X" result))))
+
+(ert-deftest test-block-banner-c-long-text ()
+ "Should handle longer text."
+ (let ((result (test-block-banner-at-column 0 "/*" "*/" "*" "This is a longer header text" 70)))
+ ;; Should have 3 lines
+ (should (= 3 (length (split-string result "\n" t))))
+ ;; Text should be present
+ (should (string-match-p "This is a longer header text" result))))
+
+(ert-deftest test-block-banner-c-custom-length ()
+ "Should respect custom length."
+ (let ((result (test-block-banner-at-column 0 "/*" "*/" "*" "Header" 50)))
+ ;; Top line should be approximately 50 chars
+ (let ((first-line (car (split-string result "\n" t))))
+ (should (<= (length first-line) 51))
+ (should (>= (length first-line) 48)))))
+
+;;; Boundary Cases
+
+(ert-deftest test-block-banner-c-minimum-length ()
+ "Should work with minimum viable length."
+ (let ((result (test-block-banner-at-column 0 "/*" "*/" "*" "X" 10)))
+ (should (= 3 (length (split-string result "\n" t))))
+ (should (string-match-p "X" result))))
+
+(ert-deftest test-block-banner-c-very-long-length ()
+ "Should handle very long length."
+ (let ((result (test-block-banner-at-column 0 "/*" "*/" "*" "Header" 200)))
+ (should (= 3 (length (split-string result "\n" t))))
+ ;; Top line should be very long
+ (let ((first-line (car (split-string result "\n" t))))
+ (should (> (length first-line) 100)))))
+
+(ert-deftest test-block-banner-c-unicode-decoration ()
+ "Should handle unicode decoration character."
+ (let ((result (test-block-banner-at-column 0 "/*" "*/" "✦" "Header" 70)))
+ (should (string-match-p "✦" result))))
+
+(ert-deftest test-block-banner-c-unicode-text ()
+ "Should handle unicode in text."
+ (let ((result (test-block-banner-at-column 0 "/*" "*/" "*" "Hello 👋 مرحبا café" 70)))
+ (should (string-match-p "👋" result))
+ (should (string-match-p "مرحبا" result))
+ (should (string-match-p "café" result))))
+
+(ert-deftest test-block-banner-c-very-long-text ()
+ "Should handle very long text."
+ (let* ((long-text (make-string 100 ?x))
+ (result (test-block-banner-at-column 0 "/*" "*/" "*" long-text 70)))
+ ;; Should still generate output
+ (should (= 3 (length (split-string result "\n" t))))
+ ;; Middle line should contain some of the text
+ (should (string-match-p "xxx" result))))
+
+(ert-deftest test-block-banner-c-max-indentation ()
+ "Should handle maximum practical indentation."
+ (let ((result (test-block-banner-at-column 60 "/*" "*/" "*" "Header" 100)))
+ (should (= 3 (length (split-string result "\n" t))))
+ ;; First line should start with 60 spaces
+ (should (string-prefix-p (make-string 60 ?\s) result))))
+
+;;; Error Cases
+
+(ert-deftest test-block-banner-c-length-too-small ()
+ "Should error when length is too small."
+ (should-error
+ (test-block-banner-at-column 0 "/*" "*/" "*" "Header" 3)
+ :type 'error))
+
+(ert-deftest test-block-banner-c-negative-length ()
+ "Should error with negative length."
+ (should-error
+ (test-block-banner-at-column 0 "/*" "*/" "*" "Header" -10)
+ :type 'error))
+
+(ert-deftest test-block-banner-c-zero-length ()
+ "Should error with zero length."
+ (should-error
+ (test-block-banner-at-column 0 "/*" "*/" "*" "Header" 0)
+ :type 'error))
+
+(ert-deftest test-block-banner-c-nil-decoration ()
+ "Should error when decoration-char is nil."
+ (should-error
+ (test-block-banner-at-column 0 "/*" "*/" nil "Header" 70)
+ :type 'wrong-type-argument))
+
+(ert-deftest test-block-banner-c-nil-text ()
+ "Should error when text is nil."
+ (should-error
+ (test-block-banner-at-column 0 "/*" "*/" "*" nil 70)
+ :type 'wrong-type-argument))
+
+(ert-deftest test-block-banner-c-non-integer-length ()
+ "Should error when length is not an integer."
+ (should-error
+ (test-block-banner-at-column 0 "/*" "*/" "*" "Header" "not-a-number")
+ :type 'wrong-type-argument))
+
+;;; Alternative Block Comment Styles
+
+(ert-deftest test-block-banner-java-style ()
+ "Should work with Java-style block comments."
+ (let ((result (test-block-banner-at-column 0 "/**" "*/" "*" "JavaDoc Comment" 70)))
+ (should (= 3 (length (split-string result "\n" t))))
+ (should (string-match-p "^/\\*\\*\\*" result))
+ (should (string-match-p "JavaDoc Comment" result))))
+
+(ert-deftest test-block-banner-js-style ()
+ "Should work with JavaScript-style block comments."
+ (let ((result (test-block-banner-at-column 2 "/*" "*/" "*" "Function Documentation" 70)))
+ (should (= 3 (length (split-string result "\n" t))))
+ (should (string-prefix-p " /*" result))
+ (should (string-match-p "Function Documentation" result))))
+
+(provide 'test-custom-comments-comment-block-banner)
+;;; test-custom-comments-comment-block-banner.el ends here
diff --git a/tests/test-custom-comments-comment-box.el b/tests/test-custom-comments-comment-box.el
new file mode 100644
index 00000000..10b1a67d
--- /dev/null
+++ b/tests/test-custom-comments-comment-box.el
@@ -0,0 +1,241 @@
+;;; test-custom-comments-comment-box.el --- Tests for cj/comment-box -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; Tests for the cj/comment-box function from custom-comments.el
+;;
+;; This function generates a 3-line box comment:
+;; - Top border: comment-start + full decoration line
+;; - Text line: comment-start + decoration + spaces + text + spaces + decoration
+;; - Bottom border: comment-start + full decoration line
+;;
+;; The text is centered within the box with decoration characters on the sides.
+;;
+;; We test the NON-INTERACTIVE implementation (cj/--comment-box)
+;; to avoid mocking user prompts. This follows our testing best practice
+;; of separating business logic from UI interaction.
+;;
+;; Cross-Language Testing Strategy:
+;; - Comprehensive testing in Emacs Lisp (our primary language)
+;; - Representative testing in Python and C (hash-based and C-style comments)
+;; - Function handles comment syntax generically, so testing 3 syntaxes
+;; proves cross-language compatibility
+;; - See test-custom-comments-delete-buffer-comments.el for detailed rationale
+
+;;; Code:
+
+(require 'ert)
+(require 'testutil-general)
+
+;; Add modules directory to load path
+(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory))
+
+;; Stub dependencies before loading the module
+(defvar cj/custom-keymap (make-sparse-keymap)
+ "Stub keymap for testing.")
+
+;; Now load the actual production module
+(require 'custom-comments)
+
+;;; Test Helpers
+
+(defun test-comment-box-at-column (column-pos comment-start comment-end decoration-char text length)
+ "Test cj/--comment-box at COLUMN-POS indentation.
+Insert spaces to reach COLUMN-POS, then call cj/--comment-box with
+COMMENT-START, COMMENT-END, DECORATION-CHAR, TEXT, and LENGTH.
+Returns the buffer string for assertions."
+ (with-temp-buffer
+ (when (> column-pos 0)
+ (insert (make-string column-pos ?\s)))
+ (cj/--comment-box comment-start comment-end decoration-char text length)
+ (buffer-string)))
+
+;;; Emacs Lisp Tests (Primary Language - Comprehensive Coverage)
+
+;;; Normal Cases
+
+(ert-deftest test-comment-box-elisp-basic ()
+ "Should generate 3-line box in emacs-lisp style."
+ (let ((result (test-comment-box-at-column 0 ";;" "" "-" "Section Header" 70)))
+ ;; Should have 3 lines
+ (should (= 3 (length (split-string result "\n" t))))
+ ;; First line should start with ;; and have decoration
+ (should (string-match-p "^;; -" result))
+ ;; Middle line should contain text with side borders
+ (should (string-match-p ";; - .* Section Header .* - ;;" result))
+ ;; Should have top and bottom borders
+ (should (string-match-p "^;; -" result))))
+
+(ert-deftest test-comment-box-elisp-custom-decoration ()
+ "Should use custom decoration character."
+ (let ((result (test-comment-box-at-column 0 ";;" "" "*" "Header" 70)))
+ (should (string-match-p ";; \\*" result))
+ (should-not (string-match-p "-" result))))
+
+(ert-deftest test-comment-box-elisp-custom-text ()
+ "Should include custom text centered in box."
+ (let ((result (test-comment-box-at-column 0 ";;" "" "-" "Custom Text Here" 70)))
+ (should (string-match-p "Custom Text Here" result))))
+
+(ert-deftest test-comment-box-elisp-empty-text ()
+ "Should handle empty text string."
+ (let ((result (test-comment-box-at-column 0 ";;" "" "-" "" 70)))
+ ;; Should still generate 3 lines
+ (should (= 3 (length (split-string result "\n" t))))
+ ;; Should have side borders
+ (should (string-match-p "- .*-" result))))
+
+(ert-deftest test-comment-box-elisp-at-column-0 ()
+ "Should work at column 0."
+ (let ((result (test-comment-box-at-column 0 ";;" "" "-" "Header" 70)))
+ ;; First character should be semicolon
+ (should (string-prefix-p ";;" result))))
+
+(ert-deftest test-comment-box-elisp-indented ()
+ "Should work when indented."
+ (let ((result (test-comment-box-at-column 4 ";;" "" "-" "Header" 70)))
+ ;; First line should start with spaces
+ (should (string-prefix-p " ;;" result))
+ ;; Other lines should be indented
+ (let ((lines (split-string result "\n" t)))
+ (should (string-prefix-p " " (nth 1 lines)))
+ (should (string-prefix-p " " (nth 2 lines))))))
+
+(ert-deftest test-comment-box-elisp-short-text ()
+ "Should center short text properly."
+ (let ((result (test-comment-box-at-column 0 ";;" "" "-" "X" 70)))
+ ;; Should have 3 lines
+ (should (= 3 (length (split-string result "\n" t))))
+ ;; Text should be present and centered
+ (should (string-match-p "- .* X .* -" result))))
+
+(ert-deftest test-comment-box-elisp-long-text ()
+ "Should handle longer text."
+ (let ((result (test-comment-box-at-column 0 ";;" "" "-" "This is a longer header text" 70)))
+ ;; Should have 3 lines
+ (should (= 3 (length (split-string result "\n" t))))
+ ;; Text should be present
+ (should (string-match-p "This is a longer header text" result))))
+
+;;; Boundary Cases
+
+(ert-deftest test-comment-box-elisp-minimum-length ()
+ "Should work with minimum viable length."
+ (let ((result (test-comment-box-at-column 0 ";;" "" "-" "X" 15)))
+ (should (= 3 (length (split-string result "\n" t))))
+ (should (string-match-p "X" result))))
+
+(ert-deftest test-comment-box-elisp-very-long-length ()
+ "Should handle very long length."
+ (let ((result (test-comment-box-at-column 0 ";;" "" "-" "Header" 200)))
+ (should (= 3 (length (split-string result "\n" t))))
+ ;; Border lines should be very long
+ (let ((first-line (car (split-string result "\n" t))))
+ (should (> (length first-line) 100)))))
+
+(ert-deftest test-comment-box-elisp-unicode-decoration ()
+ "Should handle unicode decoration character."
+ (let ((result (test-comment-box-at-column 0 ";;" "" "═" "Header" 70)))
+ (should (string-match-p "═" result))))
+
+(ert-deftest test-comment-box-elisp-unicode-text ()
+ "Should handle unicode in text."
+ (let ((result (test-comment-box-at-column 0 ";;" "" "-" "Hello 👋 مرحبا café" 70)))
+ (should (string-match-p "👋" result))
+ (should (string-match-p "مرحبا" result))
+ (should (string-match-p "café" result))))
+
+(ert-deftest test-comment-box-elisp-very-long-text ()
+ "Should handle very long text."
+ (let* ((long-text (make-string 100 ?x))
+ (result (test-comment-box-at-column 0 ";;" "" "-" long-text 70)))
+ ;; Should still generate output
+ (should (= 3 (length (split-string result "\n" t))))
+ ;; Middle line should contain some of the text
+ (should (string-match-p "xxx" result))))
+
+(ert-deftest test-comment-box-elisp-comment-end-symmetric ()
+ "Should use symmetric comment syntax when comment-end is empty."
+ (let ((result (test-comment-box-at-column 0 ";;" "" "-" "Header" 70)))
+ (should (= 3 (length (split-string result "\n" t))))
+ ;; Should use ;; on both sides for symmetry
+ (should (string-match-p ";;.*;;$" result))))
+
+(ert-deftest test-comment-box-elisp-max-indentation ()
+ "Should handle maximum practical indentation."
+ (let ((result (test-comment-box-at-column 60 ";;" "" "-" "Header" 100)))
+ (should (= 3 (length (split-string result "\n" t))))
+ ;; First line should start with 60 spaces
+ (should (string-prefix-p (make-string 60 ?\s) result))))
+
+(ert-deftest test-comment-box-elisp-text-centering-even ()
+ "Should center text properly with even length."
+ (let ((result (test-comment-box-at-column 0 ";;" "" "-" "EVEN" 70)))
+ ;; Text should be centered (roughly equal padding on both sides)
+ (should (string-match-p "- .* EVEN .* -" result))))
+
+(ert-deftest test-comment-box-elisp-text-centering-odd ()
+ "Should center text properly with odd length."
+ (let ((result (test-comment-box-at-column 0 ";;" "" "-" "ODD" 70)))
+ ;; Text should be centered (roughly equal padding on both sides)
+ (should (string-match-p "- .* ODD .* -" result))))
+
+;;; Error Cases
+
+(ert-deftest test-comment-box-elisp-length-too-small ()
+ "Should error when length is too small."
+ (should-error
+ (test-comment-box-at-column 0 ";;" "" "-" "Header" 5)
+ :type 'error))
+
+(ert-deftest test-comment-box-elisp-negative-length ()
+ "Should error with negative length."
+ (should-error
+ (test-comment-box-at-column 0 ";;" "" "-" "Header" -10)
+ :type 'error))
+
+(ert-deftest test-comment-box-elisp-zero-length ()
+ "Should error with zero length."
+ (should-error
+ (test-comment-box-at-column 0 ";;" "" "-" "Header" 0)
+ :type 'error))
+
+(ert-deftest test-comment-box-elisp-nil-decoration ()
+ "Should error when decoration-char is nil."
+ (should-error
+ (test-comment-box-at-column 0 ";;" "" nil "Header" 70)
+ :type 'wrong-type-argument))
+
+(ert-deftest test-comment-box-elisp-non-integer-length ()
+ "Should error when length is not an integer."
+ (should-error
+ (test-comment-box-at-column 0 ";;" "" "-" "Header" "not-a-number")
+ :type 'wrong-type-argument))
+
+;;; Python Tests (Hash-based comments)
+
+(ert-deftest test-comment-box-python-basic ()
+ "Should generate box with Python comment syntax."
+ (let ((result (test-comment-box-at-column 0 "#" "" "-" "Section" 70)))
+ (should (= 3 (length (split-string result "\n" t))))
+ (should (string-match-p "^# -" result))
+ (should (string-match-p "Section" result))))
+
+(ert-deftest test-comment-box-python-indented ()
+ "Should handle indented Python comments."
+ (let ((result (test-comment-box-at-column 4 "#" "" "#" "Function Section" 70)))
+ (should (string-prefix-p " #" result))
+ (should (string-match-p "Function Section" result))))
+
+;;; C Tests (C-style comments)
+
+(ert-deftest test-comment-box-c-block-comments ()
+ "Should generate box with C block comment syntax."
+ (let ((result (test-comment-box-at-column 0 "/*" "*/" "-" "Section" 70)))
+ (should (= 3 (length (split-string result "\n" t))))
+ (should (string-match-p "^/\\* -" result))
+ (should (string-match-p "Section" result))
+ ;; Should include comment-end
+ (should (string-match-p "\\*/" result))))
+
+(provide 'test-custom-comments-comment-box)
+;;; test-custom-comments-comment-box.el ends here
diff --git a/tests/test-custom-comments-comment-heavy-box.el b/tests/test-custom-comments-comment-heavy-box.el
new file mode 100644
index 00000000..30289625
--- /dev/null
+++ b/tests/test-custom-comments-comment-heavy-box.el
@@ -0,0 +1,251 @@
+;;; test-custom-comments-comment-heavy-box.el --- Tests for cj/comment-heavy-box -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; Tests for the cj/comment-heavy-box function from custom-comments.el
+;;
+;; This function generates a 5-line heavy box comment:
+;; - Top border: comment-start + full decoration line
+;; - Empty line: decoration char + spaces + decoration char
+;; - Centered text: decoration char + spaces + text + spaces + decoration char
+;; - Empty line: decoration char + spaces + decoration char
+;; - Bottom border: comment-start + full decoration line
+;;
+;; The text is centered within the box with padding on both sides.
+;;
+;; We test the NON-INTERACTIVE implementation (cj/--comment-heavy-box)
+;; to avoid mocking user prompts. This follows our testing best practice
+;; of separating business logic from UI interaction.
+;;
+;; Cross-Language Testing Strategy:
+;; - Comprehensive testing in Emacs Lisp (our primary language)
+;; - Representative testing in Python and C (hash-based and C-style comments)
+;; - Function handles comment syntax generically, so testing 3 syntaxes
+;; proves cross-language compatibility
+;; - See test-custom-comments-delete-buffer-comments.el for detailed rationale
+
+;;; Code:
+
+(require 'ert)
+(require 'testutil-general)
+
+;; Add modules directory to load path
+(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory))
+
+;; Stub dependencies before loading the module
+(defvar cj/custom-keymap (make-sparse-keymap)
+ "Stub keymap for testing.")
+
+;; Now load the actual production module
+(require 'custom-comments)
+
+;;; Test Helpers
+
+(defun test-heavy-box-at-column (column-pos comment-start comment-end decoration-char text length)
+ "Test cj/--comment-heavy-box at COLUMN-POS indentation.
+Insert spaces to reach COLUMN-POS, then call cj/--comment-heavy-box with
+COMMENT-START, COMMENT-END, DECORATION-CHAR, TEXT, and LENGTH.
+Returns the buffer string for assertions."
+ (with-temp-buffer
+ (when (> column-pos 0)
+ (insert (make-string column-pos ?\s)))
+ (cj/--comment-heavy-box comment-start comment-end decoration-char text length)
+ (buffer-string)))
+
+;;; Emacs Lisp Tests (Primary Language - Comprehensive Coverage)
+
+;;; Normal Cases
+
+(ert-deftest test-heavy-box-elisp-basic ()
+ "Should generate 5-line heavy box in emacs-lisp style."
+ (let ((result (test-heavy-box-at-column 0 ";;" "" "*" "Section Header" 70)))
+ ;; Should have 5 lines
+ (should (= 5 (length (split-string result "\n" t))))
+ ;; First line should start with ;; and have decoration
+ (should (string-match-p "^;; \\*" result))
+ ;; Middle line should contain centered text
+ (should (string-match-p "Section Header" result))
+ ;; Should have side borders
+ (should (string-match-p "^\\*.*\\*$" result))))
+
+(ert-deftest test-heavy-box-elisp-custom-decoration ()
+ "Should use custom decoration character."
+ (let ((result (test-heavy-box-at-column 0 ";;" "" "#" "Header" 70)))
+ (should (string-match-p ";; #" result))
+ (should-not (string-match-p "\\*" result))))
+
+(ert-deftest test-heavy-box-elisp-custom-text ()
+ "Should include custom text centered in box."
+ (let ((result (test-heavy-box-at-column 0 ";;" "" "*" "Custom Text Here" 70)))
+ (should (string-match-p "Custom Text Here" result))))
+
+(ert-deftest test-heavy-box-elisp-empty-text ()
+ "Should handle empty text string."
+ (let ((result (test-heavy-box-at-column 0 ";;" "" "*" "" 70)))
+ ;; Should still generate 5 lines
+ (should (= 5 (length (split-string result "\n" t))))
+ ;; Middle line should just have side borders and spaces
+ (should (string-match-p "^\\*.*\\*$" result))))
+
+(ert-deftest test-heavy-box-elisp-at-column-0 ()
+ "Should work at column 0."
+ (let ((result (test-heavy-box-at-column 0 ";;" "" "*" "Header" 70)))
+ ;; First character should be semicolon
+ (should (string-prefix-p ";;" result))))
+
+(ert-deftest test-heavy-box-elisp-indented ()
+ "Should work when indented."
+ (let ((result (test-heavy-box-at-column 4 ";;" "" "*" "Header" 70)))
+ ;; First line should start with spaces
+ (should (string-prefix-p " ;;" result))
+ ;; Other lines should be indented
+ (let ((lines (split-string result "\n" t)))
+ (should (string-prefix-p " " (nth 1 lines)))
+ (should (string-prefix-p " " (nth 2 lines))))))
+
+(ert-deftest test-heavy-box-elisp-short-text ()
+ "Should center short text properly."
+ (let ((result (test-heavy-box-at-column 0 ";;" "" "*" "X" 70)))
+ ;; Should have 5 lines
+ (should (= 5 (length (split-string result "\n" t))))
+ ;; Text should be present and centered
+ (should (string-match-p "\\* .* X .* \\*" result))))
+
+(ert-deftest test-heavy-box-elisp-long-text ()
+ "Should handle longer text."
+ (let ((result (test-heavy-box-at-column 0 ";;" "" "*" "This is a longer header text" 70)))
+ ;; Should have 5 lines
+ (should (= 5 (length (split-string result "\n" t))))
+ ;; Text should be present
+ (should (string-match-p "This is a longer header text" result))))
+
+;;; Boundary Cases
+
+(ert-deftest test-heavy-box-elisp-minimum-length ()
+ "Should work with minimum viable length."
+ ;; Minimum for a box: comment + spaces + borders + minimal content
+ (let ((result (test-heavy-box-at-column 0 ";;" "" "*" "X" 15)))
+ (should (= 5 (length (split-string result "\n" t))))
+ (should (string-match-p "X" result))))
+
+(ert-deftest test-heavy-box-elisp-very-long-length ()
+ "Should handle very long length."
+ (let ((result (test-heavy-box-at-column 0 ";;" "" "*" "Header" 200)))
+ (should (= 5 (length (split-string result "\n" t))))
+ ;; Border lines should be very long
+ (let ((first-line (car (split-string result "\n" t))))
+ (should (> (length first-line) 100)))))
+
+(ert-deftest test-heavy-box-elisp-unicode-decoration ()
+ "Should handle unicode decoration character."
+ (let ((result (test-heavy-box-at-column 0 ";;" "" "═" "Header" 70)))
+ (should (string-match-p "═" result))))
+
+(ert-deftest test-heavy-box-elisp-unicode-text ()
+ "Should handle unicode in text."
+ (let ((result (test-heavy-box-at-column 0 ";;" "" "*" "Hello 👋 مرحبا café" 70)))
+ (should (string-match-p "👋" result))
+ (should (string-match-p "مرحبا" result))
+ (should (string-match-p "café" result))))
+
+(ert-deftest test-heavy-box-elisp-very-long-text ()
+ "Should handle very long text."
+ (let* ((long-text (make-string 100 ?x))
+ (result (test-heavy-box-at-column 0 ";;" "" "*" long-text 70)))
+ ;; Should still generate output
+ (should (= 5 (length (split-string result "\n" t))))
+ ;; Middle line should contain some of the text
+ (should (string-match-p "xxx" result))))
+
+(ert-deftest test-heavy-box-elisp-comment-end-empty ()
+ "Should handle empty comment-end by using symmetric comment syntax."
+ (let ((result (test-heavy-box-at-column 0 ";;" "" "*" "Header" 70)))
+ (should (= 5 (length (split-string result "\n" t))))
+ ;; When comment-end is empty, function uses comment-char for symmetry
+ ;; So border lines will have ";; ... ;;" for visual balance
+ (should (string-match-p ";;.*;;$" result))))
+
+(ert-deftest test-heavy-box-elisp-max-indentation ()
+ "Should handle maximum practical indentation."
+ (let ((result (test-heavy-box-at-column 60 ";;" "" "*" "Header" 100)))
+ (should (= 5 (length (split-string result "\n" t))))
+ ;; First line should start with 60 spaces
+ (should (string-prefix-p (make-string 60 ?\s) result))))
+
+(ert-deftest test-heavy-box-elisp-text-centering-even ()
+ "Should center text properly with even length."
+ (let ((result (test-heavy-box-at-column 0 ";;" "" "*" "EVEN" 70)))
+ ;; Text should be centered (roughly equal padding on both sides)
+ (should (string-match-p "\\* .* EVEN .* \\*" result))))
+
+(ert-deftest test-heavy-box-elisp-text-centering-odd ()
+ "Should center text properly with odd length."
+ (let ((result (test-heavy-box-at-column 0 ";;" "" "*" "ODD" 70)))
+ ;; Text should be centered (roughly equal padding on both sides)
+ (should (string-match-p "\\* .* ODD .* \\*" result))))
+
+;;; Error Cases
+
+(ert-deftest test-heavy-box-elisp-length-too-small ()
+ "Should error when length is too small."
+ (should-error
+ (test-heavy-box-at-column 0 ";;" "" "*" "Header" 5)
+ :type 'error))
+
+(ert-deftest test-heavy-box-elisp-negative-length ()
+ "Should error with negative length."
+ (should-error
+ (test-heavy-box-at-column 0 ";;" "" "*" "Header" -10)
+ :type 'error))
+
+(ert-deftest test-heavy-box-elisp-zero-length ()
+ "Should error with zero length."
+ (should-error
+ (test-heavy-box-at-column 0 ";;" "" "*" "Header" 0)
+ :type 'error))
+
+(ert-deftest test-heavy-box-elisp-nil-decoration ()
+ "Should error when decoration-char is nil."
+ (should-error
+ (test-heavy-box-at-column 0 ";;" "" nil "Header" 70)
+ :type 'wrong-type-argument))
+
+(ert-deftest test-heavy-box-elisp-nil-text ()
+ "Should error when text is nil."
+ (should-error
+ (test-heavy-box-at-column 0 ";;" "" "*" nil 70)
+ :type 'wrong-type-argument))
+
+(ert-deftest test-heavy-box-elisp-non-integer-length ()
+ "Should error when length is not an integer."
+ (should-error
+ (test-heavy-box-at-column 0 ";;" "" "*" "Header" "not-a-number")
+ :type 'wrong-type-argument))
+
+;;; Python Tests (Hash-based comments)
+
+(ert-deftest test-heavy-box-python-basic ()
+ "Should generate heavy box with Python comment syntax."
+ (let ((result (test-heavy-box-at-column 0 "#" "" "*" "Section" 70)))
+ (should (= 5 (length (split-string result "\n" t))))
+ (should (string-match-p "^# \\*" result))
+ (should (string-match-p "Section" result))))
+
+(ert-deftest test-heavy-box-python-indented ()
+ "Should handle indented Python comments."
+ (let ((result (test-heavy-box-at-column 4 "#" "" "#" "Function Section" 70)))
+ (should (string-prefix-p " #" result))
+ (should (string-match-p "Function Section" result))))
+
+;;; C Tests (C-style comments)
+
+(ert-deftest test-heavy-box-c-block-comments ()
+ "Should generate heavy box with C block comment syntax."
+ (let ((result (test-heavy-box-at-column 0 "/*" "*/" "*" "Section" 70)))
+ (should (= 5 (length (split-string result "\n" t))))
+ (should (string-match-p "^/\\* \\*" result))
+ (should (string-match-p "Section" result))
+ ;; Should include comment-end
+ (should (string-match-p "\\*/" result))))
+
+(provide 'test-custom-comments-comment-heavy-box)
+;;; test-custom-comments-comment-heavy-box.el ends here
diff --git a/tests/test-custom-comments-comment-inline-border.el b/tests/test-custom-comments-comment-inline-border.el
new file mode 100644
index 00000000..ca2bef06
--- /dev/null
+++ b/tests/test-custom-comments-comment-inline-border.el
@@ -0,0 +1,235 @@
+;;; test-custom-comments-comment-inline-border.el --- Tests for cj/comment-inline-border -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; Tests for the cj/comment-inline-border function from custom-comments.el
+;;
+;; This function generates a single-line centered comment with decoration borders:
+;; Format: comment-start + decoration + space + text + space + decoration + comment-end
+;; Example: ";; ======= Section Header ======="
+;;
+;; The text is centered with decoration characters on both sides. When text has
+;; odd length, the right side gets one less decoration character.
+;;
+;; We test the NON-INTERACTIVE implementation (cj/--comment-inline-border)
+;; to avoid mocking user prompts. This follows our testing best practice
+;; of separating business logic from UI interaction.
+;;
+;; Cross-Language Testing Strategy:
+;; - Comprehensive testing in Emacs Lisp (our primary language)
+;; - Representative testing in Python and C (hash-based and C-style comments)
+;; - Function handles comment syntax generically, so testing 3 syntaxes
+;; proves cross-language compatibility
+;; - See test-custom-comments-delete-buffer-comments.el for detailed rationale
+
+;;; Code:
+
+(require 'ert)
+(require 'testutil-general)
+
+;; Add modules directory to load path
+(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory))
+
+;; Stub dependencies before loading the module
+(defvar cj/custom-keymap (make-sparse-keymap)
+ "Stub keymap for testing.")
+
+;; Now load the actual production module
+(require 'custom-comments)
+
+;;; Test Helpers
+
+(defun test-inline-border-at-column (column-pos comment-start comment-end decoration-char text length)
+ "Test cj/--comment-inline-border at COLUMN-POS indentation.
+Insert spaces to reach COLUMN-POS, then call cj/--comment-inline-border with
+COMMENT-START, COMMENT-END, DECORATION-CHAR, TEXT, and LENGTH.
+Returns the buffer string for assertions."
+ (with-temp-buffer
+ (when (> column-pos 0)
+ (insert (make-string column-pos ?\s)))
+ (cj/--comment-inline-border comment-start comment-end decoration-char text length)
+ (buffer-string)))
+
+;;; Emacs Lisp Tests (Primary Language - Comprehensive Coverage)
+
+;;; Normal Cases
+
+(ert-deftest test-inline-border-elisp-basic ()
+ "Should generate single-line centered comment in emacs-lisp style."
+ (let ((result (test-inline-border-at-column 0 ";;" "" "=" "Section Header" 70)))
+ ;; Should be single line
+ (should (= 1 (length (split-string result "\n" t))))
+ ;; Should start with ;;
+ (should (string-match-p "^;; =" result))
+ ;; Should contain text
+ (should (string-match-p "Section Header" result))
+ ;; Should have decoration on both sides
+ (should (string-match-p "= Section Header =" result))))
+
+(ert-deftest test-inline-border-elisp-custom-decoration ()
+ "Should use custom decoration character."
+ (let ((result (test-inline-border-at-column 0 ";;" "" "#" "Header" 70)))
+ (should (string-match-p ";; #" result))
+ (should (string-match-p "# Header #" result))
+ (should-not (string-match-p "=" result))))
+
+(ert-deftest test-inline-border-elisp-custom-text ()
+ "Should include custom text centered."
+ (let ((result (test-inline-border-at-column 0 ";;" "" "=" "Custom Text Here" 70)))
+ (should (string-match-p "Custom Text Here" result))))
+
+(ert-deftest test-inline-border-elisp-empty-text ()
+ "Should handle empty text string."
+ (let ((result (test-inline-border-at-column 0 ";;" "" "=" "" 70)))
+ ;; Should still generate output with decoration
+ (should (string-match-p ";; =" result))
+ ;; Should not have extra spaces where text would be
+ (should-not (string-match-p " " result))))
+
+(ert-deftest test-inline-border-elisp-at-column-0 ()
+ "Should work at column 0."
+ (let ((result (test-inline-border-at-column 0 ";;" "" "=" "Header" 70)))
+ ;; First character should be semicolon
+ (should (string-prefix-p ";;" result))))
+
+(ert-deftest test-inline-border-elisp-indented ()
+ "Should work when indented."
+ (let ((result (test-inline-border-at-column 4 ";;" "" "=" "Header" 70)))
+ ;; Result should start with spaces
+ (should (string-prefix-p " ;;" result))))
+
+(ert-deftest test-inline-border-elisp-short-text ()
+ "Should center short text properly."
+ (let ((result (test-inline-border-at-column 0 ";;" "" "=" "X" 70)))
+ (should (string-match-p "X" result))
+ ;; Should have decoration on both sides
+ (should (string-match-p "= X =" result))))
+
+(ert-deftest test-inline-border-elisp-custom-length ()
+ "Should respect custom length."
+ (let ((result (test-inline-border-at-column 0 ";;" "" "=" "Header" 50)))
+ ;; Line should be approximately 50 chars
+ (let ((line (car (split-string result "\n" t))))
+ (should (<= (length line) 51))
+ (should (>= (length line) 48)))))
+
+;;; Boundary Cases
+
+(ert-deftest test-inline-border-elisp-minimum-length ()
+ "Should work with minimum viable length."
+ ;; Minimum: 2 (;;) + 1 (space) + 1 (space) + 2 (min decoration each side) = 6
+ (let ((result (test-inline-border-at-column 0 ";;" "" "=" "" 10)))
+ (should (string-match-p ";" result))))
+
+(ert-deftest test-inline-border-elisp-text-centering-even ()
+ "Should center text properly with even length."
+ (let ((result (test-inline-border-at-column 0 ";;" "" "=" "EVEN" 70)))
+ ;; Text should be centered with roughly equal decoration
+ (should (string-match-p "= EVEN =" result))))
+
+(ert-deftest test-inline-border-elisp-text-centering-odd ()
+ "Should center text properly with odd length."
+ (let ((result (test-inline-border-at-column 0 ";;" "" "=" "ODD" 70)))
+ ;; Text should be centered (right side has one less due to odd length)
+ (should (string-match-p "= ODD =" result))))
+
+(ert-deftest test-inline-border-elisp-very-long-text ()
+ "Should handle text that fills most of the line."
+ (let* ((long-text (make-string 50 ?x))
+ (result (test-inline-border-at-column 0 ";;" "" "=" long-text 70)))
+ ;; Should still have decoration
+ (should (string-match-p "=" result))
+ ;; Text should be present
+ (should (string-match-p "xxx" result))))
+
+(ert-deftest test-inline-border-elisp-unicode-decoration ()
+ "Should handle unicode decoration character."
+ (let ((result (test-inline-border-at-column 0 ";;" "" "─" "Header" 70)))
+ (should (string-match-p "─" result))))
+
+(ert-deftest test-inline-border-elisp-unicode-text ()
+ "Should handle unicode in text."
+ (let ((result (test-inline-border-at-column 0 ";;" "" "=" "Hello 👋 café" 70)))
+ (should (string-match-p "👋" result))
+ (should (string-match-p "café" result))))
+
+(ert-deftest test-inline-border-elisp-comment-end-empty ()
+ "Should handle empty comment-end correctly."
+ (let ((result (test-inline-border-at-column 0 ";;" "" "=" "Header" 70)))
+ ;; Line should not have trailing comment-end
+ (should-not (string-match-p ";;$" result))))
+
+(ert-deftest test-inline-border-elisp-max-indentation ()
+ "Should handle maximum practical indentation."
+ (let ((result (test-inline-border-at-column 60 ";;" "" "=" "H" 100)))
+ (should (string-prefix-p (make-string 60 ?\s) result))))
+
+(ert-deftest test-inline-border-elisp-minimum-decoration-each-side ()
+ "Should have at least 2 decoration chars on each side."
+ (let ((result (test-inline-border-at-column 0 ";;" "" "=" "Test" 20)))
+ ;; Should have at least == on each side
+ (should (string-match-p "== Test ==" result))))
+
+;;; Error Cases
+
+(ert-deftest test-inline-border-elisp-length-too-small ()
+ "Should error when length is too small for text."
+ (should-error
+ (test-inline-border-at-column 0 ";;" "" "=" "Very Long Header Text" 20)
+ :type 'error))
+
+(ert-deftest test-inline-border-elisp-negative-length ()
+ "Should error with negative length."
+ (should-error
+ (test-inline-border-at-column 0 ";;" "" "=" "Header" -10)
+ :type 'error))
+
+(ert-deftest test-inline-border-elisp-zero-length ()
+ "Should error with zero length."
+ (should-error
+ (test-inline-border-at-column 0 ";;" "" "=" "Header" 0)
+ :type 'error))
+
+(ert-deftest test-inline-border-elisp-nil-decoration ()
+ "Should error when decoration-char is nil."
+ (should-error
+ (test-inline-border-at-column 0 ";;" "" nil "Header" 70)
+ :type 'wrong-type-argument))
+
+(ert-deftest test-inline-border-elisp-non-integer-length ()
+ "Should error when length is not an integer."
+ (should-error
+ (test-inline-border-at-column 0 ";;" "" "=" "Header" "not-a-number")
+ :type 'wrong-type-argument))
+
+;;; Python Tests (Hash-based comments)
+
+(ert-deftest test-inline-border-python-basic ()
+ "Should generate inline border with Python comment syntax."
+ (let ((result (test-inline-border-at-column 0 "#" "" "=" "Section" 70)))
+ (should (string-match-p "^# =" result))
+ (should (string-match-p "Section" result))))
+
+(ert-deftest test-inline-border-python-indented ()
+ "Should handle indented Python comments."
+ (let ((result (test-inline-border-at-column 4 "#" "" "-" "Function Section" 70)))
+ (should (string-prefix-p " #" result))
+ (should (string-match-p "Function Section" result))))
+
+;;; C Tests (C-style comments)
+
+(ert-deftest test-inline-border-c-block-comments ()
+ "Should generate inline border with C block comment syntax."
+ (let ((result (test-inline-border-at-column 0 "/*" "*/" "=" "Section" 70)))
+ (should (string-match-p "^/\\* =" result))
+ (should (string-match-p "Section" result))
+ ;; Should include comment-end
+ (should (string-match-p "\\*/$" result))))
+
+(ert-deftest test-inline-border-c-line-comments ()
+ "Should generate inline border with C line comment syntax."
+ (let ((result (test-inline-border-at-column 0 "//" "" "-" "Header" 70)))
+ (should (string-match-p "^// -" result))
+ (should (string-match-p "Header" result))))
+
+(provide 'test-custom-comments-comment-inline-border)
+;;; test-custom-comments-comment-inline-border.el ends here
diff --git a/tests/test-custom-comments-comment-padded-divider.el b/tests/test-custom-comments-comment-padded-divider.el
new file mode 100644
index 00000000..702a4c67
--- /dev/null
+++ b/tests/test-custom-comments-comment-padded-divider.el
@@ -0,0 +1,250 @@
+;;; test-custom-comments-comment-padded-divider.el --- Tests for cj/comment-padded-divider -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; Tests for the cj/comment-padded-divider function from custom-comments.el
+;;
+;; This function generates a padded 3-line comment divider banner:
+;; - Top line: comment-start + decoration chars
+;; - Middle line: comment-start + padding spaces + text
+;; - Bottom line: comment-start + decoration chars
+;;
+;; The key difference from simple-divider is the PADDING parameter which
+;; adds spaces before the text to create visual indentation.
+;;
+;; We test the NON-INTERACTIVE implementation (cj/--comment-padded-divider)
+;; to avoid mocking user prompts. This follows our testing best practice
+;; of separating business logic from UI interaction.
+;;
+;; Cross-Language Testing Strategy:
+;; - Comprehensive testing in Emacs Lisp (our primary language)
+;; - Representative testing in Python and C (hash-based and C-style comments)
+;; - Function handles comment syntax generically, so testing 3 syntaxes
+;; proves cross-language compatibility
+;; - See test-custom-comments-delete-buffer-comments.el for detailed rationale
+
+;;; Code:
+
+(require 'ert)
+(require 'testutil-general)
+
+;; Add modules directory to load path
+(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory))
+
+;; Stub dependencies before loading the module
+(defvar cj/custom-keymap (make-sparse-keymap)
+ "Stub keymap for testing.")
+
+;; Now load the actual production module
+(require 'custom-comments)
+
+;;; Test Helpers
+
+(defun test-padded-divider-at-column (column-pos comment-start comment-end decoration-char text length padding)
+ "Test cj/--comment-padded-divider at COLUMN-POS indentation.
+Insert spaces to reach COLUMN-POS, then call cj/--comment-padded-divider with
+COMMENT-START, COMMENT-END, DECORATION-CHAR, TEXT, LENGTH, and PADDING.
+Returns the buffer string for assertions."
+ (with-temp-buffer
+ (when (> column-pos 0)
+ (insert (make-string column-pos ?\s)))
+ (cj/--comment-padded-divider comment-start comment-end decoration-char text length padding)
+ (buffer-string)))
+
+;;; Emacs Lisp Tests (Primary Language - Comprehensive Coverage)
+
+;;; Normal Cases
+
+(ert-deftest test-padded-divider-elisp-basic ()
+ "Should generate padded 3-line divider in emacs-lisp style."
+ (let ((result (test-padded-divider-at-column 0 ";;" "" "=" "Section Header" 70 2)))
+ ;; Should have 3 lines
+ (should (= 3 (length (split-string result "\n" t))))
+ ;; First line should start with ;; and have decoration
+ (should (string-match-p "^;; =" result))
+ ;; Middle line should contain text with padding
+ (should (string-match-p ";; Section Header" result))))
+
+(ert-deftest test-padded-divider-elisp-custom-padding ()
+ "Should respect custom padding value."
+ (let ((result (test-padded-divider-at-column 0 ";;" "" "=" "Header" 70 4)))
+ ;; Middle line should have 4 spaces before text
+ (should (string-match-p ";; Header" result))))
+
+(ert-deftest test-padded-divider-elisp-zero-padding ()
+ "Should work with zero padding."
+ (let ((result (test-padded-divider-at-column 0 ";;" "" "-" "Header" 70 0)))
+ ;; Middle line should have text immediately after comment-start + space
+ (should (string-match-p "^;; Header$" result))))
+
+(ert-deftest test-padded-divider-elisp-large-padding ()
+ "Should work with large padding value."
+ (let ((result (test-padded-divider-at-column 0 ";;" "" "=" "Text" 70 10)))
+ ;; Middle line should have 10 spaces before text
+ (should (string-match-p ";; Text" result))))
+
+(ert-deftest test-padded-divider-elisp-custom-decoration ()
+ "Should use custom decoration character."
+ (let ((result (test-padded-divider-at-column 0 ";;" "" "*" "Header" 70 2)))
+ (should (string-match-p ";; \\*" result))
+ (should-not (string-match-p ";; =" result))))
+
+(ert-deftest test-padded-divider-elisp-custom-text ()
+ "Should include custom text in middle line."
+ (let ((result (test-padded-divider-at-column 0 ";;" "" "=" "Custom Text Here" 70 2)))
+ (should (string-match-p "Custom Text Here" result))))
+
+(ert-deftest test-padded-divider-elisp-empty-text ()
+ "Should handle empty text string."
+ (let ((result (test-padded-divider-at-column 0 ";;" "" "-" "" 70 2)))
+ ;; Should still generate 3 lines
+ (should (= 3 (length (split-string result "\n" t))))
+ ;; Middle line should just be comment-start + padding
+ (should (string-match-p "^;; *\n" result))))
+
+(ert-deftest test-padded-divider-elisp-at-column-0 ()
+ "Should work at column 0."
+ (let ((result (test-padded-divider-at-column 0 ";;" "" "=" "Header" 70 2)))
+ ;; First character should be semicolon
+ (should (string-prefix-p ";;" result))))
+
+(ert-deftest test-padded-divider-elisp-indented ()
+ "Should work when indented."
+ (let ((result (test-padded-divider-at-column 4 ";;" "" "=" "Header" 70 2)))
+ ;; Result should start with spaces
+ (should (string-prefix-p " ;;" result))
+ ;; All lines should be indented
+ (dolist (line (split-string result "\n" t))
+ (should (string-prefix-p " ;;" line)))))
+
+;;; Boundary Cases
+
+(ert-deftest test-padded-divider-elisp-minimum-length ()
+ "Should work with minimum viable length at column 0."
+ ;; Minimum: 2 (;;) + 1 (space) + 1 (space) + 3 (dashes) = 7
+ (let ((result (test-padded-divider-at-column 0 ";;" "" "-" "" 7 0)))
+ (should (= 3 (length (split-string result "\n" t))))))
+
+(ert-deftest test-padded-divider-elisp-very-long-length ()
+ "Should handle very long length."
+ (let ((result (test-padded-divider-at-column 0 ";;" "" "=" "Header" 200 2)))
+ (should (= 3 (length (split-string result "\n" t))))
+ ;; Decoration lines should be very long
+ (let ((first-line (car (split-string result "\n" t))))
+ (should (> (length first-line) 100)))))
+
+(ert-deftest test-padded-divider-elisp-padding-larger-than-length ()
+ "Should handle padding that exceeds reasonable bounds."
+ ;; This tests behavior when padding is very large relative to length
+ (let ((result (test-padded-divider-at-column 0 ";;" "" "=" "X" 70 50)))
+ ;; Should still generate output (text may extend beyond decoration)
+ (should (= 3 (length (split-string result "\n" t))))
+ (should (string-match-p "X" result))))
+
+(ert-deftest test-padded-divider-elisp-unicode-decoration ()
+ "Should handle unicode decoration character."
+ (let ((result (test-padded-divider-at-column 0 ";;" "" "─" "Header" 70 2)))
+ (should (string-match-p "─" result))))
+
+(ert-deftest test-padded-divider-elisp-unicode-text ()
+ "Should handle unicode in text."
+ (let ((result (test-padded-divider-at-column 0 ";;" "" "=" "Hello 👋 مرحبا café" 70 2)))
+ (should (string-match-p "👋" result))
+ (should (string-match-p "مرحبا" result))
+ (should (string-match-p "café" result))))
+
+(ert-deftest test-padded-divider-elisp-very-long-text ()
+ "Should handle very long text."
+ (let* ((long-text (make-string 100 ?x))
+ (result (test-padded-divider-at-column 0 ";;" "" "=" long-text 70 2)))
+ ;; Should still generate output
+ (should (= 3 (length (split-string result "\n" t))))
+ ;; Middle line should contain some of the text
+ (should (string-match-p "xxx" result))))
+
+(ert-deftest test-padded-divider-elisp-comment-end-empty ()
+ "Should handle empty comment-end correctly."
+ (let ((result (test-padded-divider-at-column 0 ";;" "" "=" "Header" 70 2)))
+ (should (= 3 (length (split-string result "\n" t))))
+ ;; Lines should not have trailing comment-end
+ (should-not (string-match-p ";;.*;;$" result))))
+
+(ert-deftest test-padded-divider-elisp-max-indentation ()
+ "Should handle maximum practical indentation."
+ (let ((result (test-padded-divider-at-column 60 ";;" "" "=" "Header" 100 2)))
+ (should (= 3 (length (split-string result "\n" t))))
+ ;; All lines should start with 60 spaces
+ (dolist (line (split-string result "\n" t))
+ (should (string-prefix-p (make-string 60 ?\s) line)))))
+
+;;; Error Cases
+
+(ert-deftest test-padded-divider-elisp-negative-padding ()
+ "Should error with negative padding."
+ (should-error
+ (test-padded-divider-at-column 0 ";;" "" "=" "Header" 70 -5)
+ :type 'error))
+
+(ert-deftest test-padded-divider-elisp-negative-length ()
+ "Should error with negative length."
+ (should-error
+ (test-padded-divider-at-column 0 ";;" "" "=" "Header" -10 2)
+ :type 'error))
+
+(ert-deftest test-padded-divider-elisp-zero-length ()
+ "Should error with zero length."
+ (should-error
+ (test-padded-divider-at-column 0 ";;" "" "=" "Header" 0 2)
+ :type 'error))
+
+(ert-deftest test-padded-divider-elisp-nil-decoration ()
+ "Should error when decoration-char is nil."
+ (should-error
+ (test-padded-divider-at-column 0 ";;" "" nil "Header" 70 2)
+ :type 'wrong-type-argument))
+
+(ert-deftest test-padded-divider-elisp-nil-text ()
+ "Should error when text is nil."
+ (should-error
+ (test-padded-divider-at-column 0 ";;" "" "=" nil 70 2)
+ :type 'wrong-type-argument))
+
+(ert-deftest test-padded-divider-elisp-non-integer-length ()
+ "Should error when length is not an integer."
+ (should-error
+ (test-padded-divider-at-column 0 ";;" "" "=" "Header" "not-a-number" 2)
+ :type 'wrong-type-argument))
+
+(ert-deftest test-padded-divider-elisp-non-integer-padding ()
+ "Should error when padding is not an integer."
+ (should-error
+ (test-padded-divider-at-column 0 ";;" "" "=" "Header" 70 "not-a-number")
+ :type 'wrong-type-argument))
+
+;;; Python Tests (Hash-based comments)
+
+(ert-deftest test-padded-divider-python-basic ()
+ "Should generate padded divider with Python comment syntax."
+ (let ((result (test-padded-divider-at-column 0 "#" "" "=" "Section" 70 2)))
+ (should (= 3 (length (split-string result "\n" t))))
+ (should (string-match-p "^# =" result))
+ (should (string-match-p "# Section" result))))
+
+(ert-deftest test-padded-divider-python-indented ()
+ "Should handle indented Python comments with padding."
+ (let ((result (test-padded-divider-at-column 4 "#" "" "-" "Function Section" 70 4)))
+ (should (string-prefix-p " #" result))
+ (should (string-match-p "Function Section" result))))
+
+;;; C Tests (C-style comments)
+
+(ert-deftest test-padded-divider-c-block-comments ()
+ "Should generate padded divider with C block comment syntax."
+ (let ((result (test-padded-divider-at-column 0 "/*" "*/" "=" "Section" 70 2)))
+ (should (= 3 (length (split-string result "\n" t))))
+ (should (string-match-p "^/\\* =" result))
+ (should (string-match-p "/\\* Section" result))
+ ;; Should include comment-end
+ (should (string-match-p "\\*/" result))))
+
+(provide 'test-custom-comments-comment-padded-divider)
+;;; test-custom-comments-comment-padded-divider.el ends here
diff --git a/tests/test-custom-comments-comment-reformat.el b/tests/test-custom-comments-comment-reformat.el
new file mode 100644
index 00000000..83248aee
--- /dev/null
+++ b/tests/test-custom-comments-comment-reformat.el
@@ -0,0 +1,191 @@
+;;; test-custom-comments-comment-reformat.el --- Tests for cj/comment-reformat -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; Tests for the cj/comment-reformat function from custom-comments.el
+;;
+;; This function reformats multi-line comments into a single paragraph by:
+;; 1. Uncommenting the selected region
+;; 2. Joining lines together (via cj/join-line-or-region)
+;; 3. Re-commenting the result
+;; 4. Temporarily reducing fill-column by 3 during the join operation
+;;
+;; Dependencies:
+;; - Requires cj/join-line-or-region from custom-line-paragraph.el
+;; - We load the REAL module to test actual integration behavior
+;; - This follows our "test production code" guideline
+;; - If join-line-or-region has bugs, our tests will catch integration issues
+;;
+;; Cross-Language Testing Strategy:
+;; - Comprehensive testing in Emacs Lisp (12 tests)
+;; - Representative testing in Python and C (1 test each)
+;; - Function delegates to uncomment-region/comment-region, so we test OUR logic
+;; - See test-custom-comments-delete-buffer-comments.el for detailed rationale
+
+;;; Code:
+
+(require 'ert)
+(require 'testutil-general)
+
+;; Add modules directory to load path
+(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory))
+
+;; Stub dependencies before loading the module
+(defvar cj/custom-keymap (make-sparse-keymap)
+ "Stub keymap for testing.")
+
+;; Load the real custom-line-paragraph module (for cj/join-line-or-region)
+(require 'custom-line-paragraph)
+
+;; Now load the actual production module
+(require 'custom-comments)
+
+;;; Test Helpers
+
+(defun test-comment-reformat-in-mode (mode content-before expected-after)
+ "Test comment reformatting in MODE.
+Insert CONTENT-BEFORE, select all, run cj/comment-reformat, verify EXPECTED-AFTER."
+ (with-temp-buffer
+ (transient-mark-mode 1) ; Enable transient-mark-mode for batch testing
+ (funcall mode)
+ (insert content-before)
+ (mark-whole-buffer)
+ (activate-mark) ; Explicitly activate the mark
+ (cj/comment-reformat)
+ (should (equal (string-trim (buffer-string)) (string-trim expected-after)))))
+
+;;; Emacs Lisp Tests (Primary Language - Comprehensive Coverage)
+
+(ert-deftest test-comment-reformat-elisp-simple-multiline ()
+ "Should join multiple commented lines into one."
+ (test-comment-reformat-in-mode
+ 'emacs-lisp-mode
+ ";; Line one\n;; Line two\n;; Line three"
+ ";; Line one Line two Line three"))
+
+(ert-deftest test-comment-reformat-elisp-preserves-content ()
+ "Should preserve text content after reformat."
+ (test-comment-reformat-in-mode
+ 'emacs-lisp-mode
+ ";; Hello world\n;; from Emacs"
+ ";; Hello world from Emacs"))
+
+(ert-deftest test-comment-reformat-elisp-restores-fill-column ()
+ "Should restore fill-column after operation."
+ (with-temp-buffer
+ (transient-mark-mode 1)
+ (emacs-lisp-mode)
+ (let ((original-fill-column fill-column))
+ (insert ";; Line one\n;; Line two")
+ (mark-whole-buffer)
+ (activate-mark)
+ (cj/comment-reformat)
+ (should (= fill-column original-fill-column)))))
+
+(ert-deftest test-comment-reformat-elisp-single-line ()
+ "Should handle single commented line."
+ (test-comment-reformat-in-mode
+ 'emacs-lisp-mode
+ ";; Single line comment"
+ ";; Single line comment"))
+
+(ert-deftest test-comment-reformat-elisp-empty-region ()
+ "Should error when trying to comment empty buffer."
+ (with-temp-buffer
+ (transient-mark-mode 1)
+ (emacs-lisp-mode)
+ (mark-whole-buffer)
+ (activate-mark)
+ (should-error (cj/comment-reformat))))
+
+(ert-deftest test-comment-reformat-elisp-whitespace-in-comments ()
+ "Should handle comments with only whitespace."
+ (test-comment-reformat-in-mode
+ 'emacs-lisp-mode
+ ";; \n;; \n;; text"
+ ";; text"))
+
+(ert-deftest test-comment-reformat-elisp-unicode ()
+ "Should handle unicode in comments."
+ (test-comment-reformat-in-mode
+ 'emacs-lisp-mode
+ ";; Hello 👋\n;; مرحبا café"
+ ";; Hello 👋 مرحبا café"))
+
+(ert-deftest test-comment-reformat-elisp-long-text ()
+ "Should handle many lines of comments."
+ (test-comment-reformat-in-mode
+ 'emacs-lisp-mode
+ ";; Line 1\n;; Line 2\n;; Line 3\n;; Line 4\n;; Line 5"
+ ";; Line 1 Line 2 Line 3 Line 4 Line 5"))
+
+(ert-deftest test-comment-reformat-elisp-indented-comments ()
+ "Should handle indented comments."
+ (with-temp-buffer
+ (transient-mark-mode 1)
+ (emacs-lisp-mode)
+ (insert " ;; Indented line 1\n ;; Indented line 2")
+ (mark-whole-buffer)
+ (activate-mark)
+ (cj/comment-reformat)
+ ;; After reformatting, should still be commented
+ (should (string-match-p ";;" (buffer-string)))
+ ;; Content should be joined
+ (should (string-match-p "line 1.*line 2" (buffer-string)))))
+
+(ert-deftest test-comment-reformat-elisp-region-at-buffer-start ()
+ "Should handle region at buffer start."
+ (with-temp-buffer
+ (transient-mark-mode 1)
+ (emacs-lisp-mode)
+ (insert ";; Start line 1\n;; Start line 2\n(setq x 1)")
+ (goto-char (point-min))
+ (set-mark (point))
+ (forward-line 2)
+ (activate-mark)
+ (cj/comment-reformat)
+ (should (string-match-p ";; Start line 1.*Start line 2" (buffer-string)))))
+
+(ert-deftest test-comment-reformat-elisp-no-region-active ()
+ "Should show message when no region selected."
+ (with-temp-buffer
+ (emacs-lisp-mode)
+ (insert ";; Comment line")
+ (deactivate-mark)
+ (let ((message-log-max nil)
+ (messages '()))
+ ;; Capture messages
+ (cl-letf (((symbol-function 'message)
+ (lambda (format-string &rest args)
+ (push (apply #'format format-string args) messages))))
+ (cj/comment-reformat)
+ (should (string-match-p "No region was selected" (car messages)))))))
+
+(ert-deftest test-comment-reformat-elisp-read-only-buffer ()
+ "Should signal error in read-only buffer."
+ (with-temp-buffer
+ (emacs-lisp-mode)
+ (insert ";; Line 1\n;; Line 2")
+ (mark-whole-buffer)
+ (read-only-mode 1)
+ (should-error (cj/comment-reformat))))
+
+;;; Python Tests (Hash-based comments)
+
+(ert-deftest test-comment-reformat-python-simple ()
+ "Should join Python hash comments."
+ (test-comment-reformat-in-mode
+ 'python-mode
+ "# Line one\n# Line two"
+ "# Line one Line two"))
+
+;;; C Tests (C-style comments)
+
+(ert-deftest test-comment-reformat-c-line-comments ()
+ "Should join C line comments (C-mode converts to block comments)."
+ (test-comment-reformat-in-mode
+ 'c-mode
+ "// Line one\n// Line two"
+ "/* Line one Line two */"))
+
+(provide 'test-custom-comments-comment-reformat)
+;;; test-custom-comments-comment-reformat.el ends here
diff --git a/tests/test-custom-comments-comment-simple-divider.el b/tests/test-custom-comments-comment-simple-divider.el
new file mode 100644
index 00000000..a61e6b4c
--- /dev/null
+++ b/tests/test-custom-comments-comment-simple-divider.el
@@ -0,0 +1,246 @@
+;;; test-custom-comments-comment-simple-divider.el --- Tests for cj/comment-simple-divider -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; Tests for the cj/comment-simple-divider function from custom-comments.el
+;;
+;; This function generates a simple 3-line comment divider banner:
+;; - Top line: comment-start + decoration chars
+;; - Middle line: comment-start + text
+;; - Bottom line: comment-start + decoration chars
+;;
+;; We test the NON-INTERACTIVE implementation (cj/--comment-simple-divider)
+;; to avoid mocking user prompts. This follows our testing best practice
+;; of separating business logic from UI interaction.
+;;
+;; Cross-Language Testing Strategy:
+;; - Comprehensive testing in Emacs Lisp (our primary language)
+;; - Representative testing in Python and C (hash-based and C-style comments)
+;; - Function handles comment syntax generically, so testing 3 syntaxes
+;; proves cross-language compatibility
+;; - See test-custom-comments-delete-buffer-comments.el for detailed rationale
+
+;;; Code:
+
+(require 'ert)
+(require 'testutil-general)
+
+;; Add modules directory to load path
+(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory))
+
+;; Stub dependencies before loading the module
+(defvar cj/custom-keymap (make-sparse-keymap)
+ "Stub keymap for testing.")
+
+;; Now load the actual production module
+(require 'custom-comments)
+
+;;; Test Helpers
+
+(defun test-simple-divider-at-column (column-pos comment-start comment-end decoration-char text length)
+ "Test cj/--comment-simple-divider at COLUMN-POS indentation.
+Insert spaces to reach COLUMN-POS, then call cj/--comment-simple-divider with
+COMMENT-START, COMMENT-END, DECORATION-CHAR, TEXT, and LENGTH.
+Returns the buffer string for assertions."
+ (with-temp-buffer
+ (when (> column-pos 0)
+ (insert (make-string column-pos ?\s)))
+ (cj/--comment-simple-divider comment-start comment-end decoration-char text length)
+ (buffer-string)))
+
+;;; Emacs Lisp Tests (Primary Language - Comprehensive Coverage)
+
+;;; Normal Cases
+
+(ert-deftest test-simple-divider-elisp-basic ()
+ "Should generate simple 3-line divider in emacs-lisp style."
+ (let ((result (test-simple-divider-at-column 0 ";;" "" "-" "Section Header" 70)))
+ ;; Should have 3 lines
+ (should (= 3 (length (split-string result "\n" t))))
+ ;; Each line should start with ;;
+ (should (string-match-p "^;; -" result))
+ ;; Middle line should contain text
+ (should (string-match-p ";; Section Header" result))))
+
+(ert-deftest test-simple-divider-elisp-custom-decoration ()
+ "Should use custom decoration character."
+ (let ((result (test-simple-divider-at-column 0 ";;" "" "=" "Header" 70)))
+ (should (string-match-p ";; =" result))
+ (should-not (string-match-p ";; -" result))))
+
+(ert-deftest test-simple-divider-elisp-custom-text ()
+ "Should include custom text in middle line."
+ (let ((result (test-simple-divider-at-column 0 ";;" "" "-" "Custom Text Here" 70)))
+ (should (string-match-p ";; Custom Text Here" result))))
+
+(ert-deftest test-simple-divider-elisp-custom-length ()
+ "Should respect custom length."
+ (let* ((result (test-simple-divider-at-column 0 ";;" "" "-" "Header" 50))
+ (lines (split-string result "\n" t)))
+ ;; Should have 3 lines
+ (should (= 3 (length lines)))
+ ;; First and last lines (decoration) should be approximately 50 chars
+ (should (<= (length (car lines)) 51))
+ (should (>= (length (car lines)) 48))
+ (should (<= (length (car (last lines))) 51))
+ (should (>= (length (car (last lines))) 48))))
+
+(ert-deftest test-simple-divider-elisp-empty-text ()
+ "Should handle empty text string."
+ (let ((result (test-simple-divider-at-column 0 ";;" "" "-" "" 70)))
+ ;; Should still generate 3 lines
+ (should (= 3 (length (split-string result "\n" t))))
+ ;; Middle line should just be comment-start
+ (should (string-match-p "^;; *\n" result))))
+
+(ert-deftest test-simple-divider-elisp-at-column-0 ()
+ "Should work at column 0."
+ (let ((result (test-simple-divider-at-column 0 ";;" "" "-""Header" 70)))
+ ;; First character should be semicolon
+ (should (string-prefix-p ";;" result))))
+
+(ert-deftest test-simple-divider-elisp-indented ()
+ "Should work when indented."
+ (let ((result (test-simple-divider-at-column 4 ";;" "" "-""Header" 70)))
+ ;; Result should start with spaces
+ (should (string-prefix-p " ;;" result))
+ ;; All lines should be indented
+ (dolist (line (split-string result "\n" t))
+ (should (string-prefix-p " ;;" line)))))
+
+;;; Boundary Cases
+
+(ert-deftest test-simple-divider-elisp-minimum-length ()
+ "Should work with minimum viable length at column 0."
+ ;; Minimum length at column 0: 2 (;;) + 1 (space) + 1 (space) + 3 (dashes) = 7
+ (let ((result (test-simple-divider-at-column 0 ";;" "" "-""" 7)))
+ (should (= 3 (length (split-string result "\n" t))))))
+
+(ert-deftest test-simple-divider-elisp-minimum-length-indented ()
+ "Should work with minimum viable length when indented."
+ ;; At column 4, minimum is 4 + 2 + 1 + 1 + 3 = 11
+ (let ((result (test-simple-divider-at-column 4 ";;" "" "-""" 11)))
+ (should (= 3 (length (split-string result "\n" t))))))
+
+(ert-deftest test-simple-divider-elisp-very-long-length ()
+ "Should handle very long length."
+ (let ((result (test-simple-divider-at-column 0 ";;" "" "-""Header" 200)))
+ (should (= 3 (length (split-string result "\n" t))))
+ ;; Decoration lines should be very long
+ (let ((first-line (car (split-string result "\n" t))))
+ (should (> (length first-line) 100)))))
+
+(ert-deftest test-simple-divider-elisp-unicode-decoration ()
+ "Should handle unicode decoration character."
+ (let ((result (test-simple-divider-at-column 0 ";;" "" "─""Header" 70)))
+ (should (string-match-p "─" result))))
+
+(ert-deftest test-simple-divider-elisp-unicode-text ()
+ "Should handle unicode in text."
+ (let ((result (test-simple-divider-at-column 0 ";;" "" "-""Hello 👋 مرحبا café" 70)))
+ (should (string-match-p "👋" result))
+ (should (string-match-p "مرحبا" result))
+ (should (string-match-p "café" result))))
+
+(ert-deftest test-simple-divider-elisp-very-long-text ()
+ "Should handle very long text (may wrap or truncate)."
+ (let* ((long-text (make-string 100 ?x))
+ (result (test-simple-divider-at-column 0 ";;" "" "-"long-text 70)))
+ ;; Should still generate output (behavior may vary)
+ (should (= 3 (length (split-string result "\n" t))))
+ ;; Middle line should contain some of the text
+ (should (string-match-p "xxx" result))))
+
+(ert-deftest test-simple-divider-elisp-comment-end-empty ()
+ "Should handle empty comment-end correctly."
+ (let ((result (test-simple-divider-at-column 0 ";;" "" "-""Header" 70)))
+ (should (= 3 (length (split-string result "\n" t))))
+ ;; Lines should not have trailing comment-end
+ (should-not (string-match-p ";;.*;;$" result))))
+
+(ert-deftest test-simple-divider-elisp-max-indentation ()
+ "Should handle maximum practical indentation."
+ (let ((result (test-simple-divider-at-column 60 ";;" "" "-""Header" 100)))
+ (should (= 3 (length (split-string result "\n" t))))
+ ;; All lines should start with 60 spaces
+ (dolist (line (split-string result "\n" t))
+ (should (string-prefix-p (make-string 60 ?\s) line)))))
+
+;;; Error Cases
+
+(ert-deftest test-simple-divider-elisp-length-too-small-column-0 ()
+ "Should error when length is too small at column 0."
+ (should-error
+ (test-simple-divider-at-column 0 ";;" "" "-" "Header" 5)
+ :type 'error))
+
+(ert-deftest test-simple-divider-elisp-length-too-small-indented ()
+ "Should error when length is too small for indentation level."
+ (should-error
+ (test-simple-divider-at-column 10 ";;" "" "-" "Header" 15)
+ :type 'error))
+
+(ert-deftest test-simple-divider-elisp-negative-length ()
+ "Should error with negative length."
+ (should-error
+ (test-simple-divider-at-column 0 ";;" "" "-" "Header" -10)
+ :type 'error))
+
+(ert-deftest test-simple-divider-elisp-zero-length ()
+ "Should error with zero length."
+ (should-error
+ (test-simple-divider-at-column 0 ";;" "" "-" "Header" 0)
+ :type 'error))
+
+(ert-deftest test-simple-divider-elisp-nil-decoration ()
+ "Should error when decoration-char is nil."
+ (should-error
+ (test-simple-divider-at-column 0 ";;" "" nil "Header" 70)
+ :type 'wrong-type-argument))
+
+(ert-deftest test-simple-divider-elisp-nil-text ()
+ "Should error when text is nil."
+ (should-error
+ (test-simple-divider-at-column 0 ";;" "" "-" nil 70)
+ :type 'wrong-type-argument))
+
+(ert-deftest test-simple-divider-elisp-non-integer-length ()
+ "Should error when length is not an integer."
+ (should-error
+ (test-simple-divider-at-column 0 ";;" "" "-""Header" "not-a-number")
+ :type 'wrong-type-argument))
+
+;;; Python Tests (Hash-based comments)
+
+(ert-deftest test-simple-divider-python-basic ()
+ "Should generate simple divider with Python comment syntax."
+ (let ((result (test-simple-divider-at-column 0 "#" "" "-""Section" 70)))
+ (should (= 3 (length (split-string result "\n" t))))
+ (should (string-match-p "^# -" result))
+ (should (string-match-p "# Section" result))))
+
+(ert-deftest test-simple-divider-python-indented ()
+ "Should handle indented Python comments."
+ (let ((result (test-simple-divider-at-column 4 "#" "" "=""Function Section" 70)))
+ (should (string-prefix-p " #" result))
+ (should (string-match-p "Function Section" result))))
+
+;;; C Tests (C-style comments)
+
+(ert-deftest test-simple-divider-c-block-comments ()
+ "Should generate simple divider with C block comment syntax."
+ (let ((result (test-simple-divider-at-column 0 "/*" "*/" "-""Section" 70)))
+ (should (= 3 (length (split-string result "\n" t))))
+ (should (string-match-p "^/\\* -" result))
+ (should (string-match-p "/\\* Section" result))
+ ;; Should include comment-end
+ (should (string-match-p "\\*/" result))))
+
+(ert-deftest test-simple-divider-c-line-comments ()
+ "Should generate simple divider with C line comment syntax."
+ (let ((result (test-simple-divider-at-column 0 "//" "" "=""Header" 70)))
+ (should (= 3 (length (split-string result "\n" t))))
+ (should (string-match-p "^// =" result))
+ (should (string-match-p "// Header" result))))
+
+(provide 'test-custom-comments-comment-simple-divider)
+;;; test-custom-comments-comment-simple-divider.el ends here
diff --git a/tests/test-custom-comments-comment-unicode-box.el b/tests/test-custom-comments-comment-unicode-box.el
new file mode 100644
index 00000000..f34329c8
--- /dev/null
+++ b/tests/test-custom-comments-comment-unicode-box.el
@@ -0,0 +1,264 @@
+;;; test-custom-comments-comment-unicode-box.el --- Tests for cj/comment-unicode-box -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; Tests for the cj/comment-unicode-box function from custom-comments.el
+;;
+;; This function generates a 3-line unicode box comment:
+;; - Top line: comment-start + top-left corner + horizontal lines + top-right corner
+;; - Text line: comment-start + vertical bar + text + vertical bar
+;; - Bottom line: comment-start + bottom-left corner + horizontal lines + bottom-right corner
+;;
+;; Supports both 'single and 'double box styles with different unicode characters.
+;;
+;; We test the NON-INTERACTIVE implementation (cj/--comment-unicode-box)
+;; to avoid mocking user prompts. This follows our testing best practice
+;; of separating business logic from UI interaction.
+;;
+;; Cross-Language Testing Strategy:
+;; - Comprehensive testing in Emacs Lisp (our primary language)
+;; - Representative testing in Python and C (hash-based and C-style comments)
+;; - Function handles comment syntax generically, so testing 3 syntaxes
+;; proves cross-language compatibility
+;; - See test-custom-comments-delete-buffer-comments.el for detailed rationale
+
+;;; Code:
+
+(require 'ert)
+(require 'testutil-general)
+
+;; Add modules directory to load path
+(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory))
+
+;; Stub dependencies before loading the module
+(defvar cj/custom-keymap (make-sparse-keymap)
+ "Stub keymap for testing.")
+
+;; Now load the actual production module
+(require 'custom-comments)
+
+;;; Test Helpers
+
+(defun test-unicode-box-at-column (column-pos comment-start comment-end text length box-style)
+ "Test cj/--comment-unicode-box at COLUMN-POS indentation.
+Insert spaces to reach COLUMN-POS, then call cj/--comment-unicode-box with
+COMMENT-START, COMMENT-END, TEXT, LENGTH, and BOX-STYLE.
+Returns the buffer string for assertions."
+ (with-temp-buffer
+ (when (> column-pos 0)
+ (insert (make-string column-pos ?\s)))
+ (cj/--comment-unicode-box comment-start comment-end text length box-style)
+ (buffer-string)))
+
+;;; Emacs Lisp Tests (Primary Language - Comprehensive Coverage)
+
+;;; Normal Cases - Single Box Style
+
+(ert-deftest test-unicode-box-elisp-single-basic ()
+ "Should generate 3-line single-line unicode box in emacs-lisp style."
+ (let ((result (test-unicode-box-at-column 0 ";;" "" "Section Header" 70 'single)))
+ ;; Should have 3 lines
+ (should (= 3 (length (split-string result "\n" t))))
+ ;; Should have single-line box characters
+ (should (string-match-p "┌" result))
+ (should (string-match-p "┐" result))
+ (should (string-match-p "└" result))
+ (should (string-match-p "┘" result))
+ (should (string-match-p "─" result))
+ (should (string-match-p "│" result))
+ ;; Should contain text
+ (should (string-match-p "Section Header" result))))
+
+(ert-deftest test-unicode-box-elisp-double-basic ()
+ "Should generate 3-line double-line unicode box in emacs-lisp style."
+ (let ((result (test-unicode-box-at-column 0 ";;" "" "Section Header" 70 'double)))
+ ;; Should have 3 lines
+ (should (= 3 (length (split-string result "\n" t))))
+ ;; Should have double-line box characters
+ (should (string-match-p "╔" result))
+ (should (string-match-p "╗" result))
+ (should (string-match-p "╚" result))
+ (should (string-match-p "╝" result))
+ (should (string-match-p "═" result))
+ (should (string-match-p "║" result))
+ ;; Should contain text
+ (should (string-match-p "Section Header" result))))
+
+(ert-deftest test-unicode-box-elisp-single-vs-double ()
+ "Should use different characters for single vs double."
+ (let ((single-result (test-unicode-box-at-column 0 ";;" "" "Header" 70 'single))
+ (double-result (test-unicode-box-at-column 0 ";;" "" "Header" 70 'double)))
+ ;; Single should have single-line chars but not double
+ (should (string-match-p "─" single-result))
+ (should-not (string-match-p "═" single-result))
+ ;; Double should have double-line chars but not single
+ (should (string-match-p "═" double-result))
+ (should-not (string-match-p "─" double-result))))
+
+(ert-deftest test-unicode-box-elisp-custom-text ()
+ "Should include custom text in box."
+ (let ((result (test-unicode-box-at-column 0 ";;" "" "Custom Text Here" 70 'single)))
+ (should (string-match-p "Custom Text Here" result))))
+
+(ert-deftest test-unicode-box-elisp-empty-text ()
+ "Should handle empty text string."
+ (let ((result (test-unicode-box-at-column 0 ";;" "" "" 70 'single)))
+ ;; Should still generate 3 lines
+ (should (= 3 (length (split-string result "\n" t))))
+ ;; Should have box characters
+ (should (string-match-p "┌" result))))
+
+(ert-deftest test-unicode-box-elisp-at-column-0 ()
+ "Should work at column 0."
+ (let ((result (test-unicode-box-at-column 0 ";;" "" "Header" 70 'single)))
+ ;; First character should be semicolon
+ (should (string-prefix-p ";;" result))))
+
+(ert-deftest test-unicode-box-elisp-indented ()
+ "Should work when indented."
+ (let ((result (test-unicode-box-at-column 4 ";;" "" "Header" 70 'single)))
+ ;; Result should start with spaces
+ (should (string-prefix-p " ;;" result))
+ ;; All lines should be indented
+ (dolist (line (split-string result "\n" t))
+ (should (string-prefix-p " ;;" line)))))
+
+(ert-deftest test-unicode-box-elisp-short-text ()
+ "Should handle short text properly."
+ (let ((result (test-unicode-box-at-column 0 ";;" "" "X" 70 'single)))
+ ;; Should have 3 lines
+ (should (= 3 (length (split-string result "\n" t))))
+ ;; Text should be present
+ (should (string-match-p "X" result))))
+
+(ert-deftest test-unicode-box-elisp-long-text ()
+ "Should handle longer text."
+ (let ((result (test-unicode-box-at-column 0 ";;" "" "This is a longer header text" 70 'single)))
+ ;; Should have 3 lines
+ (should (= 3 (length (split-string result "\n" t))))
+ ;; Text should be present
+ (should (string-match-p "This is a longer header text" result))))
+
+;;; Boundary Cases
+
+(ert-deftest test-unicode-box-elisp-minimum-length ()
+ "Should work with minimum viable length."
+ (let ((result (test-unicode-box-at-column 0 ";;" "" "X" 15 'single)))
+ (should (= 3 (length (split-string result "\n" t))))
+ (should (string-match-p "X" result))))
+
+(ert-deftest test-unicode-box-elisp-very-long-length ()
+ "Should handle very long length."
+ (let ((result (test-unicode-box-at-column 0 ";;" "" "Header" 200 'single)))
+ (should (= 3 (length (split-string result "\n" t))))
+ ;; Border lines should be very long
+ (let ((first-line (car (split-string result "\n" t))))
+ (should (> (length first-line) 100)))))
+
+(ert-deftest test-unicode-box-elisp-unicode-text ()
+ "Should handle unicode in text."
+ (let ((result (test-unicode-box-at-column 0 ";;" "" "Hello 👋 مرحبا café" 70 'single)))
+ (should (string-match-p "👋" result))
+ (should (string-match-p "مرحبا" result))
+ (should (string-match-p "café" result))))
+
+(ert-deftest test-unicode-box-elisp-very-long-text ()
+ "Should handle very long text."
+ (let* ((long-text (make-string 100 ?x))
+ (result (test-unicode-box-at-column 0 ";;" "" long-text 70 'single)))
+ ;; Should still generate output
+ (should (= 3 (length (split-string result "\n" t))))
+ ;; Middle line should contain some of the text
+ (should (string-match-p "xxx" result))))
+
+(ert-deftest test-unicode-box-elisp-comment-end-empty ()
+ "Should handle empty comment-end correctly."
+ (let ((result (test-unicode-box-at-column 0 ";;" "" "Header" 70 'single)))
+ (should (= 3 (length (split-string result "\n" t))))
+ ;; Lines should not have trailing comment-end
+ (should-not (string-match-p ";;.*;;$" result))))
+
+(ert-deftest test-unicode-box-elisp-max-indentation ()
+ "Should handle maximum practical indentation."
+ (let ((result (test-unicode-box-at-column 60 ";;" "" "Header" 100 'single)))
+ (should (= 3 (length (split-string result "\n" t))))
+ ;; All lines should start with 60 spaces
+ (dolist (line (split-string result "\n" t))
+ (should (string-prefix-p (make-string 60 ?\s) line)))))
+
+;;; Error Cases
+
+(ert-deftest test-unicode-box-elisp-length-too-small ()
+ "Should error when length is too small."
+ (should-error
+ (test-unicode-box-at-column 0 ";;" "" "Header" 5 'single)
+ :type 'error))
+
+(ert-deftest test-unicode-box-elisp-negative-length ()
+ "Should error with negative length."
+ (should-error
+ (test-unicode-box-at-column 0 ";;" "" "Header" -10 'single)
+ :type 'error))
+
+(ert-deftest test-unicode-box-elisp-zero-length ()
+ "Should error with zero length."
+ (should-error
+ (test-unicode-box-at-column 0 ";;" "" "Header" 0 'single)
+ :type 'error))
+
+(ert-deftest test-unicode-box-elisp-nil-text ()
+ "Should error when text is nil."
+ (should-error
+ (test-unicode-box-at-column 0 ";;" "" nil 70 'single)
+ :type 'wrong-type-argument))
+
+(ert-deftest test-unicode-box-elisp-non-integer-length ()
+ "Should error when length is not an integer."
+ (should-error
+ (test-unicode-box-at-column 0 ";;" "" "Header" "not-a-number" 'single)
+ :type 'wrong-type-argument))
+
+(ert-deftest test-unicode-box-elisp-invalid-box-style ()
+ "Should handle invalid box-style gracefully."
+ ;; Function may use a default or error - either is acceptable
+ (let ((result (test-unicode-box-at-column 0 ";;" "" "Header" 70 'invalid)))
+ ;; Should still generate some output
+ (should (stringp result))))
+
+;;; Python Tests (Hash-based comments)
+
+(ert-deftest test-unicode-box-python-single ()
+ "Should generate unicode box with Python comment syntax."
+ (let ((result (test-unicode-box-at-column 0 "#" "" "Section" 70 'single)))
+ (should (= 3 (length (split-string result "\n" t))))
+ (should (string-match-p "^# ┌" result))
+ (should (string-match-p "Section" result))))
+
+(ert-deftest test-unicode-box-python-double ()
+ "Should generate double-line unicode box with Python comment syntax."
+ (let ((result (test-unicode-box-at-column 0 "#" "" "Section" 70 'double)))
+ (should (= 3 (length (split-string result "\n" t))))
+ (should (string-match-p "^# ╔" result))
+ (should (string-match-p "Section" result))))
+
+;;; C Tests (C-style comments)
+
+(ert-deftest test-unicode-box-c-block-comments-single ()
+ "Should generate unicode box with C block comment syntax."
+ (let ((result (test-unicode-box-at-column 0 "/*" "*/" "Section" 70 'single)))
+ (should (= 3 (length (split-string result "\n" t))))
+ (should (string-match-p "^/\\* ┌" result))
+ (should (string-match-p "Section" result))
+ ;; Should include comment-end
+ (should (string-match-p "\\*/" result))))
+
+(ert-deftest test-unicode-box-c-block-comments-double ()
+ "Should generate double-line unicode box with C block comment syntax."
+ (let ((result (test-unicode-box-at-column 0 "/*" "*/" "Section" 70 'double)))
+ (should (= 3 (length (split-string result "\n" t))))
+ (should (string-match-p "^/\\* ╔" result))
+ (should (string-match-p "Section" result))
+ ;; Should include comment-end
+ (should (string-match-p "\\*/" result))))
+
+(provide 'test-custom-comments-comment-unicode-box)
+;;; test-custom-comments-comment-unicode-box.el ends here
diff --git a/tests/test-custom-comments-delete-buffer-comments.el b/tests/test-custom-comments-delete-buffer-comments.el
new file mode 100644
index 00000000..a21386f9
--- /dev/null
+++ b/tests/test-custom-comments-delete-buffer-comments.el
@@ -0,0 +1,224 @@
+;;; test-custom-comments-delete-buffer-comments.el --- Tests for cj/delete-buffer-comments -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; Tests for the cj/delete-buffer-comments function from custom-comments.el
+;;
+;; This function deletes all comments in the current buffer by delegating to
+;; Emacs' built-in `comment-kill` function.
+;;
+;; Cross-Language Testing Strategy:
+;; --------------------------------
+;; This function works across multiple programming languages/major modes because
+;; it delegates to `comment-kill`, which respects each mode's comment syntax
+;; (comment-start, comment-end).
+;;
+;; Rather than testing exhaustively in every language (8+ languages = 100+ tests),
+;; we test strategically:
+;;
+;; 1. EXTENSIVE testing in Emacs Lisp (our primary language):
+;; - ~15 tests covering all normal/boundary/error cases
+;; - Tests edge cases: empty buffers, inline comments, unicode, etc.
+;;
+;; 2. REPRESENTATIVE testing in Python and C:
+;; - ~3 tests each proving different comment syntaxes work
+;; - Python: hash-based comments (#)
+;; - C: C-style line (//) and block (/* */) comments
+;;
+;; Why this approach?
+;; - OUR code is simple: (goto-char (point-min)) + (comment-kill ...)
+;; - We're testing OUR integration logic, not Emacs' comment-kill implementation
+;; - After proving 3 different syntaxes work, additional languages have
+;; diminishing returns (testing Emacs internals, not our code)
+;; - Avoids test suite bloat (21 tests vs 100+) while maintaining confidence
+;; - Groups languages by similarity: C-style covers C/Java/Go/JavaScript/Rust
+;;
+;; See ai-prompts/quality-engineer.org: "Testing Framework/Library Integration"
+
+;;; Code:
+
+(require 'ert)
+(require 'testutil-general)
+
+;; Add modules directory to load path
+(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory))
+
+;; Stub dependencies before loading the module
+(defvar cj/custom-keymap (make-sparse-keymap)
+ "Stub keymap for testing.")
+
+;; Now load the actual production module
+(require 'custom-comments)
+
+;;; Test Helper
+
+(defun test-delete-comments-in-mode (mode content-before expected-after)
+ "Test comment deletion in MODE.
+Insert CONTENT-BEFORE, run cj/delete-buffer-comments, verify EXPECTED-AFTER."
+ (with-temp-buffer
+ (funcall mode)
+ (insert content-before)
+ (cj/delete-buffer-comments)
+ (should (equal (string-trim (buffer-string)) (string-trim expected-after)))))
+
+;;; Emacs Lisp Tests (Primary Language - Comprehensive Coverage)
+
+(ert-deftest test-delete-comments-elisp-simple-line-comments ()
+ "Should delete simple line comments in emacs-lisp-mode."
+ (test-delete-comments-in-mode
+ 'emacs-lisp-mode
+ ";; This is a comment\n(defun foo () nil)"
+ "(defun foo () nil)"))
+
+(ert-deftest test-delete-comments-elisp-inline-comments ()
+ "Should delete inline/end-of-line comments."
+ (test-delete-comments-in-mode
+ 'emacs-lisp-mode
+ "(setq x 10) ;; set x to 10"
+ "(setq x 10)"))
+
+(ert-deftest test-delete-comments-elisp-only-comments ()
+ "Buffer with only comments should become empty."
+ (test-delete-comments-in-mode
+ 'emacs-lisp-mode
+ ";; Comment 1\n;; Comment 2\n;; Comment 3"
+ ""))
+
+(ert-deftest test-delete-comments-elisp-mixed-code-and-comments ()
+ "Should preserve code and delete all comments."
+ (test-delete-comments-in-mode
+ 'emacs-lisp-mode
+ ";; Header comment\n(defun foo ()\n ;; body comment\n (+ 1 2)) ;; inline"
+ "(defun foo ()\n\n (+ 1 2))"))
+
+(ert-deftest test-delete-comments-elisp-empty-buffer ()
+ "Should do nothing in empty buffer."
+ (test-delete-comments-in-mode
+ 'emacs-lisp-mode
+ ""
+ ""))
+
+(ert-deftest test-delete-comments-elisp-no-comments ()
+ "Should preserve all content when no comments exist."
+ (test-delete-comments-in-mode
+ 'emacs-lisp-mode
+ "(defun foo ()\n (+ 1 2))"
+ "(defun foo ()\n (+ 1 2))"))
+
+(ert-deftest test-delete-comments-elisp-whitespace-only-comments ()
+ "Should delete comments containing only whitespace."
+ (test-delete-comments-in-mode
+ 'emacs-lisp-mode
+ ";; \n;; \t\n(setq x 1)"
+ "(setq x 1)"))
+
+(ert-deftest test-delete-comments-elisp-unicode-in-comments ()
+ "Should handle unicode characters in comments."
+ (test-delete-comments-in-mode
+ 'emacs-lisp-mode
+ ";; Hello 👋 مرحبا café\n(setq x 1)"
+ "(setq x 1)"))
+
+(ert-deftest test-delete-comments-elisp-indented-comments ()
+ "Should delete comments at various indentation levels."
+ (test-delete-comments-in-mode
+ 'emacs-lisp-mode
+ "(defun foo ()\n ;; indented comment\n ;; more indented\n (+ 1 2))"
+ "(defun foo ()\n\n\n (+ 1 2))"))
+
+(ert-deftest test-delete-comments-elisp-special-chars-in-comments ()
+ "Should handle special characters in comments."
+ (test-delete-comments-in-mode
+ 'emacs-lisp-mode
+ ";; Special: !@#$%^&*()[]{}|\\/<>?\n(setq x 1)"
+ "(setq x 1)"))
+
+(ert-deftest test-delete-comments-elisp-point-not-at-beginning ()
+ "Should work regardless of initial point position."
+ (with-temp-buffer
+ (emacs-lisp-mode)
+ (insert ";; Comment 1\n(setq x 1)\n;; Comment 2")
+ (goto-char (point-max)) ; Point at end
+ (cj/delete-buffer-comments)
+ (should (equal (string-trim (buffer-string)) "(setq x 1)"))))
+
+(ert-deftest test-delete-comments-elisp-does-not-affect-kill-ring ()
+ "Should not add deleted comments to kill-ring."
+ (with-temp-buffer
+ (emacs-lisp-mode)
+ (insert ";; Comment\n(setq x 1)")
+ (setq kill-ring nil)
+ (cj/delete-buffer-comments)
+ (should (null kill-ring))))
+
+(ert-deftest test-delete-comments-elisp-read-only-buffer ()
+ "Should signal error in read-only buffer."
+ (with-temp-buffer
+ (emacs-lisp-mode)
+ (insert ";; Comment\n(setq x 1)")
+ (read-only-mode 1)
+ (should-error (cj/delete-buffer-comments))))
+
+(ert-deftest test-delete-comments-elisp-narrowed-buffer ()
+ "Should only affect visible region when narrowed."
+ (with-temp-buffer
+ (emacs-lisp-mode)
+ (insert ";; Comment 1\n(setq x 1)\n;; Comment 2\n(setq y 2)")
+ (goto-char (point-min))
+ (forward-line 2)
+ (narrow-to-region (point) (point-max))
+ (cj/delete-buffer-comments)
+ (widen)
+ ;; First comment should remain (was outside narrowed region)
+ ;; Second comment should be deleted
+ (should (string-match-p "Comment 1" (buffer-string)))
+ (should-not (string-match-p "Comment 2" (buffer-string)))))
+
+
+;;; Python Tests (Hash-based comments)
+
+(ert-deftest test-delete-comments-python-simple ()
+ "Should delete Python hash comments."
+ (test-delete-comments-in-mode
+ 'python-mode
+ "# This is a comment\ndef foo():\n return 42"
+ "def foo():\n return 42"))
+
+(ert-deftest test-delete-comments-python-inline ()
+ "Should delete inline Python comments."
+ (test-delete-comments-in-mode
+ 'python-mode
+ "x = 10 # set x to 10\ny = 20"
+ "x = 10\ny = 20"))
+
+(ert-deftest test-delete-comments-python-mixed ()
+ "Should preserve code and delete Python comments."
+ (test-delete-comments-in-mode
+ 'python-mode
+ "# Header\ndef foo():\n # body\n return 42 # inline"
+ "def foo():\n\n return 42"))
+
+;;; C Tests (C-style line and block comments)
+
+(ert-deftest test-delete-comments-c-line-comments ()
+ "Should delete C line comments (//)."
+ (test-delete-comments-in-mode
+ 'c-mode
+ "// This is a comment\nint main() {\n return 0;\n}"
+ "int main() {\n return 0;\n}"))
+
+(ert-deftest test-delete-comments-c-block-comments ()
+ "Should delete C block comments (/* */)."
+ (test-delete-comments-in-mode
+ 'c-mode
+ "/* Block comment */\nint x = 10;"
+ "int x = 10;"))
+
+(ert-deftest test-delete-comments-c-mixed ()
+ "Should delete both line and block comments in C."
+ (test-delete-comments-in-mode
+ 'c-mode
+ "// Line comment\n/* Block comment */\nint x = 10; // inline"
+ "int x = 10;"))
+
+(provide 'test-custom-comments-delete-buffer-comments)
+;;; test-custom-comments-delete-buffer-comments.el ends here
diff --git a/tests/test-custom-file-buffer-clear-to-bottom-of-buffer.el b/tests/test-custom-file-buffer-clear-to-bottom-of-buffer.el
new file mode 100644
index 00000000..969f9bb7
--- /dev/null
+++ b/tests/test-custom-file-buffer-clear-to-bottom-of-buffer.el
@@ -0,0 +1,163 @@
+;;; test-custom-file-buffer-clear-to-bottom-of-buffer.el --- Tests for cj/clear-to-bottom-of-buffer -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; Tests for the cj/clear-to-bottom-of-buffer function from custom-file-buffer.el
+;;
+;; This function deletes all text from point to the end of the current buffer.
+;; It does not save the deleted text in the kill ring.
+
+;;; Code:
+
+(require 'ert)
+(require 'testutil-general)
+
+;; Add modules directory to load path
+(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory))
+
+;; Stub dependencies before loading the module
+(defvar cj/custom-keymap (make-sparse-keymap)
+ "Stub keymap for testing.")
+
+;; Stub ps-print package
+(provide 'ps-print)
+
+;; Now load the actual production module
+(require 'custom-file-buffer)
+
+;;; Setup and Teardown
+
+(defun test-clear-to-bottom-setup ()
+ "Set up test environment."
+ (setq kill-ring nil))
+
+(defun test-clear-to-bottom-teardown ()
+ "Clean up test environment."
+ (setq kill-ring nil))
+
+;;; Normal Cases
+
+(ert-deftest test-clear-to-bottom-point-in-middle ()
+ "Should delete from point to end when point in middle."
+ (test-clear-to-bottom-setup)
+ (unwind-protect
+ (with-temp-buffer
+ (insert "Line 1\nLine 2\nLine 3")
+ (goto-char (point-min))
+ (forward-line 1) ; Point at start of "Line 2"
+ (cj/clear-to-bottom-of-buffer)
+ (should (equal (buffer-string) "Line 1\n")))
+ (test-clear-to-bottom-teardown)))
+
+(ert-deftest test-clear-to-bottom-empty-buffer ()
+ "Should do nothing in empty buffer."
+ (test-clear-to-bottom-setup)
+ (unwind-protect
+ (with-temp-buffer
+ (cj/clear-to-bottom-of-buffer)
+ (should (equal (buffer-string) "")))
+ (test-clear-to-bottom-teardown)))
+
+;;; Boundary Cases
+
+(ert-deftest test-clear-to-bottom-point-at-beginning ()
+ "Should delete entire buffer when point at beginning."
+ (test-clear-to-bottom-setup)
+ (unwind-protect
+ (with-temp-buffer
+ (insert "Line 1\nLine 2\nLine 3")
+ (goto-char (point-min))
+ (cj/clear-to-bottom-of-buffer)
+ (should (equal (buffer-string) "")))
+ (test-clear-to-bottom-teardown)))
+
+(ert-deftest test-clear-to-bottom-point-at-end ()
+ "Should delete nothing when point at end."
+ (test-clear-to-bottom-setup)
+ (unwind-protect
+ (with-temp-buffer
+ (insert "Line 1\nLine 2\nLine 3")
+ (goto-char (point-max))
+ (cj/clear-to-bottom-of-buffer)
+ (should (equal (buffer-string) "Line 1\nLine 2\nLine 3")))
+ (test-clear-to-bottom-teardown)))
+
+(ert-deftest test-clear-to-bottom-point-second-to-last-char ()
+ "Should delete last character when point at second-to-last."
+ (test-clear-to-bottom-setup)
+ (unwind-protect
+ (with-temp-buffer
+ (insert "Hello")
+ (goto-char (1- (point-max))) ; Before 'o'
+ (cj/clear-to-bottom-of-buffer)
+ (should (equal (buffer-string) "Hell")))
+ (test-clear-to-bottom-teardown)))
+
+(ert-deftest test-clear-to-bottom-unicode-content ()
+ "Should handle unicode content."
+ (test-clear-to-bottom-setup)
+ (unwind-protect
+ (with-temp-buffer
+ (insert "Hello 👋\nمرحبا\nWorld")
+ (goto-char (point-min))
+ (forward-line 1)
+ (cj/clear-to-bottom-of-buffer)
+ (should (equal (buffer-string) "Hello 👋\n")))
+ (test-clear-to-bottom-teardown)))
+
+(ert-deftest test-clear-to-bottom-narrowed-buffer ()
+ "Should respect narrowing."
+ (test-clear-to-bottom-setup)
+ (unwind-protect
+ (with-temp-buffer
+ (insert "Line 1\nLine 2\nLine 3\nLine 4")
+ (goto-char (point-min))
+ (forward-line 1)
+ (let ((start (point)))
+ (forward-line 2)
+ (narrow-to-region start (point))
+ (goto-char (point-min))
+ (forward-line 1) ; Point at "Line 3"
+ (cj/clear-to-bottom-of-buffer)
+ (should (equal (buffer-string) "Line 2\n"))))
+ (test-clear-to-bottom-teardown)))
+
+(ert-deftest test-clear-to-bottom-multiple-windows ()
+ "Should update all windows showing buffer."
+ (test-clear-to-bottom-setup)
+ (unwind-protect
+ (with-temp-buffer
+ (insert "Line 1\nLine 2\nLine 3")
+ (goto-char (point-min))
+ (forward-line 1)
+ (cj/clear-to-bottom-of-buffer)
+ ;; Just verify content changed
+ (should (equal (buffer-string) "Line 1\n")))
+ (test-clear-to-bottom-teardown)))
+
+(ert-deftest test-clear-to-bottom-does-not-affect-kill-ring ()
+ "Should not add deleted text to kill ring."
+ (test-clear-to-bottom-setup)
+ (unwind-protect
+ (with-temp-buffer
+ (insert "Line 1\nLine 2\nLine 3")
+ (goto-char (point-min))
+ (setq kill-ring nil)
+ (cj/clear-to-bottom-of-buffer)
+ (should (null kill-ring)))
+ (test-clear-to-bottom-teardown)))
+
+;;; Error Cases
+
+(ert-deftest test-clear-to-bottom-read-only-buffer ()
+ "Should signal error in read-only buffer."
+ (test-clear-to-bottom-setup)
+ (unwind-protect
+ (with-temp-buffer
+ (insert "Read-only content")
+ (read-only-mode 1)
+ (goto-char (point-min))
+ (should-error (cj/clear-to-bottom-of-buffer)))
+ (test-clear-to-bottom-teardown)))
+
+(provide 'test-custom-file-buffer-clear-to-bottom-of-buffer)
+;;; test-custom-file-buffer-clear-to-bottom-of-buffer.el ends here
diff --git a/tests/test-custom-file-buffer-clear-to-top-of-buffer.el b/tests/test-custom-file-buffer-clear-to-top-of-buffer.el
new file mode 100644
index 00000000..18e3f71b
--- /dev/null
+++ b/tests/test-custom-file-buffer-clear-to-top-of-buffer.el
@@ -0,0 +1,162 @@
+;;; test-custom-file-buffer-clear-to-top-of-buffer.el --- Tests for cj/clear-to-top-of-buffer -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; Tests for the cj/clear-to-top-of-buffer function from custom-file-buffer.el
+;;
+;; This function deletes all text from point to the beginning of the current buffer.
+;; It does not save the deleted text in the kill ring.
+
+;;; Code:
+
+(require 'ert)
+(require 'testutil-general)
+
+;; Add modules directory to load path
+(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory))
+
+;; Stub dependencies before loading the module
+(defvar cj/custom-keymap (make-sparse-keymap)
+ "Stub keymap for testing.")
+
+;; Stub ps-print package
+(provide 'ps-print)
+
+;; Now load the actual production module
+(require 'custom-file-buffer)
+
+;;; Setup and Teardown
+
+(defun test-clear-to-top-setup ()
+ "Set up test environment."
+ (setq kill-ring nil))
+
+(defun test-clear-to-top-teardown ()
+ "Clean up test environment."
+ (setq kill-ring nil))
+
+;;; Normal Cases
+
+(ert-deftest test-clear-to-top-point-in-middle ()
+ "Should delete from beginning to point when point in middle."
+ (test-clear-to-top-setup)
+ (unwind-protect
+ (with-temp-buffer
+ (insert "Line 1\nLine 2\nLine 3")
+ (goto-char (point-min))
+ (forward-line 2) ; Point at start of "Line 3"
+ (cj/clear-to-top-of-buffer)
+ (should (equal (buffer-string) "Line 3")))
+ (test-clear-to-top-teardown)))
+
+(ert-deftest test-clear-to-top-empty-buffer ()
+ "Should do nothing in empty buffer."
+ (test-clear-to-top-setup)
+ (unwind-protect
+ (with-temp-buffer
+ (cj/clear-to-top-of-buffer)
+ (should (equal (buffer-string) "")))
+ (test-clear-to-top-teardown)))
+
+;;; Boundary Cases
+
+(ert-deftest test-clear-to-top-point-at-beginning ()
+ "Should delete nothing when point at beginning."
+ (test-clear-to-top-setup)
+ (unwind-protect
+ (with-temp-buffer
+ (insert "Line 1\nLine 2\nLine 3")
+ (goto-char (point-min))
+ (cj/clear-to-top-of-buffer)
+ (should (equal (buffer-string) "Line 1\nLine 2\nLine 3")))
+ (test-clear-to-top-teardown)))
+
+(ert-deftest test-clear-to-top-point-at-end ()
+ "Should delete entire buffer when point at end."
+ (test-clear-to-top-setup)
+ (unwind-protect
+ (with-temp-buffer
+ (insert "Line 1\nLine 2\nLine 3")
+ (goto-char (point-max))
+ (cj/clear-to-top-of-buffer)
+ (should (equal (buffer-string) "")))
+ (test-clear-to-top-teardown)))
+
+(ert-deftest test-clear-to-top-point-at-second-char ()
+ "Should delete first character when point at second."
+ (test-clear-to-top-setup)
+ (unwind-protect
+ (with-temp-buffer
+ (insert "Hello")
+ (goto-char (1+ (point-min))) ; After 'H'
+ (cj/clear-to-top-of-buffer)
+ (should (equal (buffer-string) "ello")))
+ (test-clear-to-top-teardown)))
+
+(ert-deftest test-clear-to-top-unicode-content ()
+ "Should handle unicode content."
+ (test-clear-to-top-setup)
+ (unwind-protect
+ (with-temp-buffer
+ (insert "Hello 👋\nمرحبا\nWorld")
+ (goto-char (point-min))
+ (forward-line 2)
+ (cj/clear-to-top-of-buffer)
+ (should (equal (buffer-string) "World")))
+ (test-clear-to-top-teardown)))
+
+(ert-deftest test-clear-to-top-narrowed-buffer ()
+ "Should respect narrowing."
+ (test-clear-to-top-setup)
+ (unwind-protect
+ (with-temp-buffer
+ (insert "Line 1\nLine 2\nLine 3\nLine 4")
+ (goto-char (point-min))
+ (forward-line 1)
+ (let ((start (point)))
+ (forward-line 2)
+ (narrow-to-region start (point))
+ (goto-char (point-min))
+ (forward-line 1) ; Point at "Line 3"
+ (cj/clear-to-top-of-buffer)
+ (should (equal (buffer-string) "Line 3\n"))))
+ (test-clear-to-top-teardown)))
+
+(ert-deftest test-clear-to-top-multiple-windows ()
+ "Should update all windows showing buffer."
+ (test-clear-to-top-setup)
+ (unwind-protect
+ (with-temp-buffer
+ (insert "Line 1\nLine 2\nLine 3")
+ (goto-char (point-max))
+ (cj/clear-to-top-of-buffer)
+ ;; Just verify content changed
+ (should (equal (buffer-string) "")))
+ (test-clear-to-top-teardown)))
+
+(ert-deftest test-clear-to-top-does-not-affect-kill-ring ()
+ "Should not add deleted text to kill ring."
+ (test-clear-to-top-setup)
+ (unwind-protect
+ (with-temp-buffer
+ (insert "Line 1\nLine 2\nLine 3")
+ (goto-char (point-max))
+ (setq kill-ring nil)
+ (cj/clear-to-top-of-buffer)
+ (should (null kill-ring)))
+ (test-clear-to-top-teardown)))
+
+;;; Error Cases
+
+(ert-deftest test-clear-to-top-read-only-buffer ()
+ "Should signal error in read-only buffer."
+ (test-clear-to-top-setup)
+ (unwind-protect
+ (with-temp-buffer
+ (insert "Read-only content")
+ (read-only-mode 1)
+ (goto-char (point-max))
+ (should-error (cj/clear-to-top-of-buffer)))
+ (test-clear-to-top-teardown)))
+
+(provide 'test-custom-file-buffer-clear-to-top-of-buffer)
+;;; test-custom-file-buffer-clear-to-top-of-buffer.el ends here
diff --git a/tests/test-custom-file-buffer-copy-link-to-buffer-file.el b/tests/test-custom-file-buffer-copy-link-to-buffer-file.el
new file mode 100644
index 00000000..94d1e01e
--- /dev/null
+++ b/tests/test-custom-file-buffer-copy-link-to-buffer-file.el
@@ -0,0 +1,209 @@
+;;; test-custom-file-buffer-copy-link-to-buffer-file.el --- Tests for cj/copy-link-to-buffer-file -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; Tests for the cj/copy-link-to-buffer-file function from custom-file-buffer.el
+;;
+;; This function copies the full file:// path of the current buffer's file to
+;; the kill ring. For non-file buffers, it does nothing (no error).
+
+;;; Code:
+
+(require 'ert)
+(require 'testutil-general)
+
+;; Add modules directory to load path
+(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory))
+
+;; Stub dependencies before loading the module
+(defvar cj/custom-keymap (make-sparse-keymap)
+ "Stub keymap for testing.")
+
+;; Stub ps-print package
+(provide 'ps-print)
+
+;; Now load the actual production module
+(require 'custom-file-buffer)
+
+;;; Setup and Teardown
+
+(defun test-copy-link-setup ()
+ "Set up test environment."
+ (setq kill-ring nil))
+
+(defun test-copy-link-teardown ()
+ "Clean up test environment."
+ ;; Kill all buffers visiting files in the test directory
+ (dolist (buf (buffer-list))
+ (when (buffer-file-name buf)
+ (when (string-prefix-p cj/test-base-dir (buffer-file-name buf))
+ (with-current-buffer buf
+ (set-buffer-modified-p nil)
+ (kill-buffer buf)))))
+ (cj/delete-test-base-dir)
+ (setq kill-ring nil))
+
+;;; Normal Cases
+
+(ert-deftest test-copy-link-simple-file ()
+ "Should copy file:// link for simple file buffer."
+ (test-copy-link-setup)
+ (unwind-protect
+ (let* ((test-dir (cj/create-test-subdirectory "test"))
+ (test-file (expand-file-name "test.txt" test-dir)))
+ (with-temp-file test-file
+ (insert "content"))
+ (with-current-buffer (find-file test-file)
+ (cj/copy-link-to-buffer-file)
+ (should (equal (car kill-ring) (concat "file://" test-file)))))
+ (test-copy-link-teardown)))
+
+(ert-deftest test-copy-link-non-file-buffer ()
+ "Should do nothing for non-file buffer without error."
+ (test-copy-link-setup)
+ (unwind-protect
+ (with-temp-buffer
+ (setq kill-ring nil)
+ (cj/copy-link-to-buffer-file)
+ (should (null kill-ring)))
+ (test-copy-link-teardown)))
+
+;;; Boundary Cases
+
+(ert-deftest test-copy-link-unicode-filename ()
+ "Should handle unicode in filename."
+ (test-copy-link-setup)
+ (unwind-protect
+ (let* ((test-dir (cj/create-test-subdirectory "test"))
+ (test-file (expand-file-name "café.txt" test-dir)))
+ (with-temp-file test-file
+ (insert "content"))
+ (with-current-buffer (find-file test-file)
+ (cj/copy-link-to-buffer-file)
+ (should (equal (car kill-ring) (concat "file://" test-file)))))
+ (test-copy-link-teardown)))
+
+(ert-deftest test-copy-link-spaces-in-filename ()
+ "Should handle spaces in filename."
+ (test-copy-link-setup)
+ (unwind-protect
+ (let* ((test-dir (cj/create-test-subdirectory "test"))
+ (test-file (expand-file-name "my file.txt" test-dir)))
+ (with-temp-file test-file
+ (insert "content"))
+ (with-current-buffer (find-file test-file)
+ (cj/copy-link-to-buffer-file)
+ (should (equal (car kill-ring) (concat "file://" test-file)))))
+ (test-copy-link-teardown)))
+
+(ert-deftest test-copy-link-special-chars-filename ()
+ "Should handle special characters in filename."
+ (test-copy-link-setup)
+ (unwind-protect
+ (let* ((test-dir (cj/create-test-subdirectory "test"))
+ (test-file (expand-file-name "[test]-(1).txt" test-dir)))
+ (with-temp-file test-file
+ (insert "content"))
+ (with-current-buffer (find-file test-file)
+ (cj/copy-link-to-buffer-file)
+ (should (equal (car kill-ring) (concat "file://" test-file)))))
+ (test-copy-link-teardown)))
+
+(ert-deftest test-copy-link-very-long-path ()
+ "Should handle very long path."
+ (test-copy-link-setup)
+ (unwind-protect
+ (let* ((test-dir (cj/create-test-subdirectory "test"))
+ (long-name (make-string 200 ?x))
+ (test-file (expand-file-name (concat long-name ".txt") test-dir)))
+ (with-temp-file test-file
+ (insert "content"))
+ (with-current-buffer (find-file test-file)
+ (cj/copy-link-to-buffer-file)
+ (should (equal (car kill-ring) (concat "file://" test-file)))))
+ (test-copy-link-teardown)))
+
+(ert-deftest test-copy-link-hidden-file ()
+ "Should handle hidden file."
+ (test-copy-link-setup)
+ (unwind-protect
+ (let* ((test-dir (cj/create-test-subdirectory "test"))
+ (test-file (expand-file-name ".hidden" test-dir)))
+ (with-temp-file test-file
+ (insert "content"))
+ (with-current-buffer (find-file test-file)
+ (cj/copy-link-to-buffer-file)
+ (should (equal (car kill-ring) (concat "file://" test-file)))))
+ (test-copy-link-teardown)))
+
+(ert-deftest test-copy-link-no-extension ()
+ "Should handle file with no extension."
+ (test-copy-link-setup)
+ (unwind-protect
+ (let* ((test-dir (cj/create-test-subdirectory "test"))
+ (test-file (expand-file-name "README" test-dir)))
+ (with-temp-file test-file
+ (insert "content"))
+ (with-current-buffer (find-file test-file)
+ (cj/copy-link-to-buffer-file)
+ (should (equal (car kill-ring) (concat "file://" test-file)))))
+ (test-copy-link-teardown)))
+
+(ert-deftest test-copy-link-symlink-file ()
+ "Should use buffer's filename for symlink."
+ (test-copy-link-setup)
+ (unwind-protect
+ (let* ((test-dir (cj/create-test-subdirectory "test"))
+ (target-file (expand-file-name "target.txt" test-dir))
+ (link-file (expand-file-name "link.txt" test-dir)))
+ (with-temp-file target-file
+ (insert "content"))
+ (make-symbolic-link target-file link-file)
+ (with-current-buffer (find-file link-file)
+ (cj/copy-link-to-buffer-file)
+ ;; Should use the link name (what buffer-file-name returns)
+ (should (equal (car kill-ring) (concat "file://" (buffer-file-name))))))
+ (test-copy-link-teardown)))
+
+(ert-deftest test-copy-link-kill-ring-has-content ()
+ "Should add to kill ring when it already has content."
+ (test-copy-link-setup)
+ (unwind-protect
+ (let* ((test-dir (cj/create-test-subdirectory "test"))
+ (test-file (expand-file-name "test.txt" test-dir)))
+ (with-temp-file test-file
+ (insert "content"))
+ (kill-new "existing content")
+ (with-current-buffer (find-file test-file)
+ (cj/copy-link-to-buffer-file)
+ (should (equal (car kill-ring) (concat "file://" test-file)))
+ (should (equal (cadr kill-ring) "existing content"))))
+ (test-copy-link-teardown)))
+
+(ert-deftest test-copy-link-empty-kill-ring ()
+ "Should populate empty kill ring."
+ (test-copy-link-setup)
+ (unwind-protect
+ (let* ((test-dir (cj/create-test-subdirectory "test"))
+ (test-file (expand-file-name "test.txt" test-dir)))
+ (with-temp-file test-file
+ (insert "content"))
+ (setq kill-ring nil)
+ (with-current-buffer (find-file test-file)
+ (cj/copy-link-to-buffer-file)
+ (should (equal (car kill-ring) (concat "file://" test-file)))
+ (should (= (length kill-ring) 1))))
+ (test-copy-link-teardown)))
+
+(ert-deftest test-copy-link-scratch-buffer ()
+ "Should do nothing for *scratch* buffer."
+ (test-copy-link-setup)
+ (unwind-protect
+ (progn
+ (setq kill-ring nil)
+ (with-current-buffer "*scratch*"
+ (cj/copy-link-to-buffer-file)
+ (should (null kill-ring))))
+ (test-copy-link-teardown)))
+
+(provide 'test-custom-file-buffer-copy-link-to-buffer-file)
+;;; test-custom-file-buffer-copy-link-to-buffer-file.el ends here
diff --git a/tests/test-custom-file-buffer-copy-path-to-buffer-file-as-kill.el b/tests/test-custom-file-buffer-copy-path-to-buffer-file-as-kill.el
new file mode 100644
index 00000000..e7a6f64b
--- /dev/null
+++ b/tests/test-custom-file-buffer-copy-path-to-buffer-file-as-kill.el
@@ -0,0 +1,205 @@
+;;; test-custom-file-buffer-copy-path-to-buffer-file-as-kill.el --- Tests for cj/copy-path-to-buffer-file-as-kill -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; Tests for the cj/copy-path-to-buffer-file-as-kill function from custom-file-buffer.el
+;;
+;; This function copies the full path of the current buffer's file to the kill ring
+;; and returns the path. It signals an error if the buffer is not visiting a file.
+
+;;; Code:
+
+(require 'ert)
+(require 'testutil-general)
+
+;; Add modules directory to load path
+(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory))
+
+;; Stub dependencies before loading the module
+(defvar cj/custom-keymap (make-sparse-keymap)
+ "Stub keymap for testing.")
+
+;; Stub ps-print package
+(provide 'ps-print)
+
+;; Now load the actual production module
+(require 'custom-file-buffer)
+
+;;; Setup and Teardown
+
+(defun test-copy-path-setup ()
+ "Set up test environment."
+ (setq kill-ring nil))
+
+(defun test-copy-path-teardown ()
+ "Clean up test environment."
+ ;; Kill all buffers visiting files in the test directory
+ (dolist (buf (buffer-list))
+ (when (buffer-file-name buf)
+ (when (string-prefix-p cj/test-base-dir (buffer-file-name buf))
+ (with-current-buffer buf
+ (set-buffer-modified-p nil)
+ (kill-buffer buf)))))
+ (cj/delete-test-base-dir)
+ (setq kill-ring nil))
+
+;;; Normal Cases
+
+(ert-deftest test-copy-path-simple-file ()
+ "Should copy absolute path for simple file buffer."
+ (test-copy-path-setup)
+ (unwind-protect
+ (let* ((test-dir (cj/create-test-subdirectory "test"))
+ (test-file (expand-file-name "test.txt" test-dir)))
+ (with-temp-file test-file
+ (insert "content"))
+ (with-current-buffer (find-file test-file)
+ (let ((result (cj/copy-path-to-buffer-file-as-kill)))
+ (should (equal result test-file))
+ (should (equal (car kill-ring) test-file)))))
+ (test-copy-path-teardown)))
+
+(ert-deftest test-copy-path-returns-path ()
+ "Should return the path value."
+ (test-copy-path-setup)
+ (unwind-protect
+ (let* ((test-dir (cj/create-test-subdirectory "test"))
+ (test-file (expand-file-name "test.txt" test-dir)))
+ (with-temp-file test-file
+ (insert "content"))
+ (with-current-buffer (find-file test-file)
+ (let ((result (cj/copy-path-to-buffer-file-as-kill)))
+ (should (stringp result))
+ (should (equal result test-file)))))
+ (test-copy-path-teardown)))
+
+;;; Boundary Cases
+
+(ert-deftest test-copy-path-unicode-filename ()
+ "Should handle unicode in filename."
+ (test-copy-path-setup)
+ (unwind-protect
+ (let* ((test-dir (cj/create-test-subdirectory "test"))
+ (test-file (expand-file-name "café.txt" test-dir)))
+ (with-temp-file test-file
+ (insert "content"))
+ (with-current-buffer (find-file test-file)
+ (cj/copy-path-to-buffer-file-as-kill)
+ (should (equal (car kill-ring) test-file))))
+ (test-copy-path-teardown)))
+
+(ert-deftest test-copy-path-spaces-in-filename ()
+ "Should handle spaces in filename."
+ (test-copy-path-setup)
+ (unwind-protect
+ (let* ((test-dir (cj/create-test-subdirectory "test"))
+ (test-file (expand-file-name "my file.txt" test-dir)))
+ (with-temp-file test-file
+ (insert "content"))
+ (with-current-buffer (find-file test-file)
+ (cj/copy-path-to-buffer-file-as-kill)
+ (should (equal (car kill-ring) test-file))))
+ (test-copy-path-teardown)))
+
+(ert-deftest test-copy-path-special-chars-filename ()
+ "Should handle special characters in filename."
+ (test-copy-path-setup)
+ (unwind-protect
+ (let* ((test-dir (cj/create-test-subdirectory "test"))
+ (test-file (expand-file-name "[test]-(1).txt" test-dir)))
+ (with-temp-file test-file
+ (insert "content"))
+ (with-current-buffer (find-file test-file)
+ (cj/copy-path-to-buffer-file-as-kill)
+ (should (equal (car kill-ring) test-file))))
+ (test-copy-path-teardown)))
+
+(ert-deftest test-copy-path-very-long-path ()
+ "Should handle very long path."
+ (test-copy-path-setup)
+ (unwind-protect
+ (let* ((test-dir (cj/create-test-subdirectory "test"))
+ (long-name (make-string 200 ?x))
+ (test-file (expand-file-name (concat long-name ".txt") test-dir)))
+ (with-temp-file test-file
+ (insert "content"))
+ (with-current-buffer (find-file test-file)
+ (cj/copy-path-to-buffer-file-as-kill)
+ (should (equal (car kill-ring) test-file))))
+ (test-copy-path-teardown)))
+
+(ert-deftest test-copy-path-hidden-file ()
+ "Should handle hidden file."
+ (test-copy-path-setup)
+ (unwind-protect
+ (let* ((test-dir (cj/create-test-subdirectory "test"))
+ (test-file (expand-file-name ".hidden" test-dir)))
+ (with-temp-file test-file
+ (insert "content"))
+ (with-current-buffer (find-file test-file)
+ (cj/copy-path-to-buffer-file-as-kill)
+ (should (equal (car kill-ring) test-file))))
+ (test-copy-path-teardown)))
+
+(ert-deftest test-copy-path-no-extension ()
+ "Should handle file with no extension."
+ (test-copy-path-setup)
+ (unwind-protect
+ (let* ((test-dir (cj/create-test-subdirectory "test"))
+ (test-file (expand-file-name "README" test-dir)))
+ (with-temp-file test-file
+ (insert "content"))
+ (with-current-buffer (find-file test-file)
+ (cj/copy-path-to-buffer-file-as-kill)
+ (should (equal (car kill-ring) test-file))))
+ (test-copy-path-teardown)))
+
+(ert-deftest test-copy-path-symlink-file ()
+ "Should use buffer's filename for symlink."
+ (test-copy-path-setup)
+ (unwind-protect
+ (let* ((test-dir (cj/create-test-subdirectory "test"))
+ (target-file (expand-file-name "target.txt" test-dir))
+ (link-file (expand-file-name "link.txt" test-dir)))
+ (with-temp-file target-file
+ (insert "content"))
+ (make-symbolic-link target-file link-file)
+ (with-current-buffer (find-file link-file)
+ (cj/copy-path-to-buffer-file-as-kill)
+ (should (equal (car kill-ring) (buffer-file-name)))))
+ (test-copy-path-teardown)))
+
+(ert-deftest test-copy-path-kill-ring-has-content ()
+ "Should add to kill ring when it already has content."
+ (test-copy-path-setup)
+ (unwind-protect
+ (let* ((test-dir (cj/create-test-subdirectory "test"))
+ (test-file (expand-file-name "test.txt" test-dir)))
+ (with-temp-file test-file
+ (insert "content"))
+ (kill-new "existing content")
+ (with-current-buffer (find-file test-file)
+ (cj/copy-path-to-buffer-file-as-kill)
+ (should (equal (car kill-ring) test-file))
+ (should (equal (cadr kill-ring) "existing content"))))
+ (test-copy-path-teardown)))
+
+;;; Error Cases
+
+(ert-deftest test-copy-path-non-file-buffer ()
+ "Should signal user-error for non-file buffer."
+ (test-copy-path-setup)
+ (unwind-protect
+ (with-temp-buffer
+ (should-error (cj/copy-path-to-buffer-file-as-kill) :type 'user-error))
+ (test-copy-path-teardown)))
+
+(ert-deftest test-copy-path-scratch-buffer ()
+ "Should signal user-error for *scratch* buffer."
+ (test-copy-path-setup)
+ (unwind-protect
+ (with-current-buffer "*scratch*"
+ (should-error (cj/copy-path-to-buffer-file-as-kill) :type 'user-error))
+ (test-copy-path-teardown)))
+
+(provide 'test-custom-file-buffer-copy-path-to-buffer-file-as-kill)
+;;; test-custom-file-buffer-copy-path-to-buffer-file-as-kill.el ends here
diff --git a/tests/test-custom-file-buffer-copy-whole-buffer.el b/tests/test-custom-file-buffer-copy-whole-buffer.el
new file mode 100644
index 00000000..a0546b18
--- /dev/null
+++ b/tests/test-custom-file-buffer-copy-whole-buffer.el
@@ -0,0 +1,194 @@
+;;; test-custom-file-buffer-copy-whole-buffer.el --- Tests for cj/copy-whole-buffer -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; Tests for the cj/copy-whole-buffer function from custom-file-buffer.el
+;;
+;; This function copies the entire contents of the current buffer to the kill ring.
+;; Point and mark are left exactly where they were. No transient region is created.
+
+;;; Code:
+
+(require 'ert)
+(require 'testutil-general)
+
+;; Add modules directory to load path
+(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory))
+
+;; Stub dependencies before loading the module
+(defvar cj/custom-keymap (make-sparse-keymap)
+ "Stub keymap for testing.")
+
+;; Stub ps-print package
+(provide 'ps-print)
+
+;; Now load the actual production module
+(require 'custom-file-buffer)
+
+;;; Setup and Teardown
+
+(defun test-copy-whole-buffer-setup ()
+ "Set up test environment."
+ (setq kill-ring nil))
+
+(defun test-copy-whole-buffer-teardown ()
+ "Clean up test environment."
+ (setq kill-ring nil))
+
+;;; Normal Cases
+
+(ert-deftest test-copy-whole-buffer-simple-text ()
+ "Should copy simple text content to kill ring."
+ (test-copy-whole-buffer-setup)
+ (unwind-protect
+ (with-temp-buffer
+ (insert "Hello, world!")
+ (cj/copy-whole-buffer)
+ (should (equal (car kill-ring) "Hello, world!")))
+ (test-copy-whole-buffer-teardown)))
+
+(ert-deftest test-copy-whole-buffer-preserves-point ()
+ "Should preserve point position."
+ (test-copy-whole-buffer-setup)
+ (unwind-protect
+ (with-temp-buffer
+ (insert "Hello, world!")
+ (goto-char 7) ; Position in middle
+ (cj/copy-whole-buffer)
+ (should (= (point) 7)))
+ (test-copy-whole-buffer-teardown)))
+
+(ert-deftest test-copy-whole-buffer-preserves-mark ()
+ "Should preserve mark position."
+ (test-copy-whole-buffer-setup)
+ (unwind-protect
+ (with-temp-buffer
+ (insert "Hello, world!")
+ (push-mark 5)
+ (goto-char 10)
+ (cj/copy-whole-buffer)
+ (should (= (mark) 5))
+ (should (= (point) 10)))
+ (test-copy-whole-buffer-teardown)))
+
+;;; Boundary Cases
+
+(ert-deftest test-copy-whole-buffer-empty ()
+ "Should handle empty buffer."
+ (test-copy-whole-buffer-setup)
+ (unwind-protect
+ (with-temp-buffer
+ (cj/copy-whole-buffer)
+ (should (equal (car kill-ring) "")))
+ (test-copy-whole-buffer-teardown)))
+
+(ert-deftest test-copy-whole-buffer-large ()
+ "Should handle very large buffer."
+ (test-copy-whole-buffer-setup)
+ (unwind-protect
+ (with-temp-buffer
+ (let ((large-content (make-string 100000 ?x)))
+ (insert large-content)
+ (cj/copy-whole-buffer)
+ (should (equal (car kill-ring) large-content))))
+ (test-copy-whole-buffer-teardown)))
+
+(ert-deftest test-copy-whole-buffer-unicode ()
+ "Should handle unicode content (emoji, RTL text)."
+ (test-copy-whole-buffer-setup)
+ (unwind-protect
+ (with-temp-buffer
+ (insert "Hello 👋 مرحبا")
+ (cj/copy-whole-buffer)
+ (should (equal (car kill-ring) "Hello 👋 مرحبا")))
+ (test-copy-whole-buffer-teardown)))
+
+(ert-deftest test-copy-whole-buffer-binary ()
+ "Should handle binary content."
+ (test-copy-whole-buffer-setup)
+ (unwind-protect
+ (with-temp-buffer
+ (insert (string 0 1 2 255))
+ (cj/copy-whole-buffer)
+ (should (equal (car kill-ring) (string 0 1 2 255))))
+ (test-copy-whole-buffer-teardown)))
+
+(ert-deftest test-copy-whole-buffer-only-whitespace ()
+ "Should handle buffer with only whitespace."
+ (test-copy-whole-buffer-setup)
+ (unwind-protect
+ (with-temp-buffer
+ (insert " \t\n ")
+ (cj/copy-whole-buffer)
+ (should (equal (car kill-ring) " \t\n ")))
+ (test-copy-whole-buffer-teardown)))
+
+(ert-deftest test-copy-whole-buffer-newlines-at-boundaries ()
+ "Should handle newlines at start/end."
+ (test-copy-whole-buffer-setup)
+ (unwind-protect
+ (with-temp-buffer
+ (insert "\n\nHello\n\n")
+ (cj/copy-whole-buffer)
+ (should (equal (car kill-ring) "\n\nHello\n\n")))
+ (test-copy-whole-buffer-teardown)))
+
+(ert-deftest test-copy-whole-buffer-narrowed ()
+ "Should copy only visible region in narrowed buffer."
+ (test-copy-whole-buffer-setup)
+ (unwind-protect
+ (with-temp-buffer
+ (insert "Line 1\nLine 2\nLine 3\n")
+ (goto-char (point-min))
+ (forward-line 1)
+ (narrow-to-region (point) (progn (forward-line 1) (point)))
+ (cj/copy-whole-buffer)
+ ;; Should copy only the narrowed region
+ (should (equal (car kill-ring) "Line 2\n")))
+ (test-copy-whole-buffer-teardown)))
+
+(ert-deftest test-copy-whole-buffer-read-only ()
+ "Should work in read-only buffer."
+ (test-copy-whole-buffer-setup)
+ (unwind-protect
+ (with-temp-buffer
+ (insert "Read-only content")
+ (read-only-mode 1)
+ (cj/copy-whole-buffer)
+ (should (equal (car kill-ring) "Read-only content")))
+ (test-copy-whole-buffer-teardown)))
+
+(ert-deftest test-copy-whole-buffer-kill-ring-has-content ()
+ "Should add to kill ring when it already has content."
+ (test-copy-whole-buffer-setup)
+ (unwind-protect
+ (with-temp-buffer
+ (insert "New content")
+ (kill-new "existing content")
+ (cj/copy-whole-buffer)
+ (should (equal (car kill-ring) "New content"))
+ (should (equal (cadr kill-ring) "existing content")))
+ (test-copy-whole-buffer-teardown)))
+
+(ert-deftest test-copy-whole-buffer-multiline ()
+ "Should preserve multiline content."
+ (test-copy-whole-buffer-setup)
+ (unwind-protect
+ (with-temp-buffer
+ (insert "Line 1\nLine 2\nLine 3")
+ (cj/copy-whole-buffer)
+ (should (equal (car kill-ring) "Line 1\nLine 2\nLine 3")))
+ (test-copy-whole-buffer-teardown)))
+
+(ert-deftest test-copy-whole-buffer-no-properties ()
+ "Should strip text properties."
+ (test-copy-whole-buffer-setup)
+ (unwind-protect
+ (with-temp-buffer
+ (insert (propertize "Hello" 'face 'bold))
+ (cj/copy-whole-buffer)
+ (should (equal (car kill-ring) "Hello"))
+ (should (null (text-properties-at 0 (car kill-ring)))))
+ (test-copy-whole-buffer-teardown)))
+
+(provide 'test-custom-file-buffer-copy-whole-buffer)
+;;; test-custom-file-buffer-copy-whole-buffer.el ends here
diff --git a/tests/test-custom-file-buffer-delete-buffer-and-file.el b/tests/test-custom-file-buffer-delete-buffer-and-file.el
new file mode 100644
index 00000000..1c43ff3b
--- /dev/null
+++ b/tests/test-custom-file-buffer-delete-buffer-and-file.el
@@ -0,0 +1,671 @@
+;;; test-custom-file-buffer-delete-buffer-and-file.el --- Tests for cj/delete-buffer-and-file -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; Tests for the cj/delete-buffer-and-file function from custom-file-buffer.el
+;;
+;; This function deletes both the current buffer and the file it visits.
+;; It uses vc-delete-file for version-controlled files and delete-file
+;; for non-version-controlled files.
+;;
+;; Testing Strategy:
+;; - We test OUR code's behavior, not the underlying delete-file/vc-delete-file
+;; implementations
+;; - We verify our code correctly:
+;; 1. Detects VC vs non-VC files (via vc-backend)
+;; 2. Calls the appropriate deletion function (vc-delete-file or delete-file)
+;; 3. Passes the trash flag (t) to delete-file
+;; 4. Propagates errors from the deletion functions
+;;
+;; Why We Mock delete-file Errors:
+;; - Tests like "already deleted file" and "no delete permission" are testing
+;; system/environment behavior, not our code
+;; - The trash system handles these cases in environment-specific ways:
+;; - Missing files may not error (trash handles gracefully)
+;; - File permissions may not matter (directory permissions for moving to trash)
+;; - To make tests deterministic and portable, we mock delete-file to throw
+;; specific errors, then verify our code propagates them correctly
+;; - This tests our contract: "when delete-file fails, we let the error through"
+
+;;; Code:
+
+(require 'ert)
+(require 'testutil-general)
+
+;; Add modules directory to load path
+(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory))
+
+;; Stub dependencies before loading the module
+(defvar cj/custom-keymap (make-sparse-keymap)
+ "Stub keymap for testing.")
+
+;; Stub ps-print package
+(provide 'ps-print)
+
+;; Now load the actual production module
+(require 'custom-file-buffer)
+
+;;; Setup and Teardown
+
+(defun test-delete-buffer-and-file-setup ()
+ "Setup for delete-buffer-and-file tests."
+ (cj/create-test-base-dir))
+
+(defun test-delete-buffer-and-file-teardown ()
+ "Teardown for delete-buffer-and-file tests."
+ ;; Kill all buffers visiting files in test directory
+ (dolist (buf (buffer-list))
+ (when (buffer-file-name buf)
+ (when (string-prefix-p cj/test-base-dir (buffer-file-name buf))
+ (with-current-buffer buf
+ (set-buffer-modified-p nil))
+ (kill-buffer buf))))
+ (cj/delete-test-base-dir))
+
+;;; Normal Cases
+
+(ert-deftest test-delete-buffer-and-file-simple-delete ()
+ "Should delete file and kill buffer."
+ (test-delete-buffer-and-file-setup)
+ (unwind-protect
+ (let* ((test-dir (cj/create-test-subdirectory "test"))
+ (test-file (expand-file-name "test.txt" test-dir)))
+ (with-temp-file test-file
+ (insert "content"))
+ (find-file test-file)
+ (let ((buf (current-buffer)))
+ ;; Mock vc-backend to return nil (non-VC file)
+ (cl-letf (((symbol-function 'vc-backend) (lambda (&rest _) nil)))
+ (cj/delete-buffer-and-file)
+ (should-not (file-exists-p test-file))
+ (should-not (buffer-live-p buf)))))
+ (test-delete-buffer-and-file-teardown)))
+
+(ert-deftest test-delete-buffer-and-file-removes-file-from-disk ()
+ "Should remove file from disk."
+ (test-delete-buffer-and-file-setup)
+ (unwind-protect
+ (let* ((test-dir (cj/create-test-subdirectory "test"))
+ (test-file (expand-file-name "test.txt" test-dir)))
+ (with-temp-file test-file
+ (insert "content"))
+ (find-file test-file)
+ (cl-letf (((symbol-function 'vc-backend) (lambda (&rest _) nil)))
+ (cj/delete-buffer-and-file)
+ (should-not (file-exists-p test-file))))
+ (test-delete-buffer-and-file-teardown)))
+
+(ert-deftest test-delete-buffer-and-file-kills-buffer ()
+ "Should kill the buffer."
+ (test-delete-buffer-and-file-setup)
+ (unwind-protect
+ (let* ((test-dir (cj/create-test-subdirectory "test"))
+ (test-file (expand-file-name "test.txt" test-dir)))
+ (with-temp-file test-file
+ (insert "content"))
+ (find-file test-file)
+ (let ((buf (current-buffer)))
+ (cl-letf (((symbol-function 'vc-backend) (lambda (&rest _) nil)))
+ (cj/delete-buffer-and-file)
+ (should-not (buffer-live-p buf)))))
+ (test-delete-buffer-and-file-teardown)))
+
+(ert-deftest test-delete-buffer-and-file-calls-delete-file-with-trash-flag ()
+ "Should call delete-file with trash flag set to t."
+ (test-delete-buffer-and-file-setup)
+ (unwind-protect
+ (let* ((test-dir (cj/create-test-subdirectory "test"))
+ (test-file (expand-file-name "test.txt" test-dir))
+ (delete-file-args nil))
+ (with-temp-file test-file
+ (insert "content"))
+ (find-file test-file)
+ (cl-letf (((symbol-function 'vc-backend) (lambda (&rest _) nil))
+ ((symbol-function 'delete-file)
+ (lambda (file trash)
+ (setq delete-file-args (list file trash)))))
+ (cj/delete-buffer-and-file)
+ (should (equal delete-file-args (list test-file t)))))
+ (test-delete-buffer-and-file-teardown)))
+
+(ert-deftest test-delete-buffer-and-file-shows-message ()
+ "Should display message for non-VC deletes."
+ (test-delete-buffer-and-file-setup)
+ (unwind-protect
+ (let* ((test-dir (cj/create-test-subdirectory "test"))
+ (test-file (expand-file-name "test.txt" test-dir))
+ (message-output nil))
+ (with-temp-file test-file
+ (insert "content"))
+ (find-file test-file)
+ (cl-letf (((symbol-function 'vc-backend) (lambda (&rest _) nil))
+ ((symbol-function 'message)
+ (lambda (fmt &rest args)
+ (setq message-output (apply #'format fmt args)))))
+ (cj/delete-buffer-and-file)
+ (should (string-match-p "Deleted file.*test.txt" message-output))))
+ (test-delete-buffer-and-file-teardown)))
+
+(ert-deftest test-delete-buffer-and-file-vc-file-uses-vc-delete ()
+ "Should call vc-delete-file for VC files."
+ (test-delete-buffer-and-file-setup)
+ (unwind-protect
+ (let* ((test-dir (cj/create-test-subdirectory "test"))
+ (test-file (expand-file-name "test.txt" test-dir))
+ (vc-delete-called nil))
+ (with-temp-file test-file
+ (insert "content"))
+ (find-file test-file)
+ (cl-letf (((symbol-function 'vc-backend) (lambda (&rest _) 'Git))
+ ((symbol-function 'vc-delete-file)
+ (lambda (file)
+ (setq vc-delete-called file)
+ ;; Simulate vc-delete-file killing the buffer
+ (when (get-file-buffer file)
+ (kill-buffer (get-file-buffer file)))
+ ;; Actually delete the file for test cleanup
+ (delete-file file t))))
+ (cj/delete-buffer-and-file)
+ (should (string= vc-delete-called test-file))))
+ (test-delete-buffer-and-file-teardown)))
+
+(ert-deftest test-delete-buffer-and-file-non-vc-file-uses-delete-file ()
+ "Should call delete-file for non-VC files."
+ (test-delete-buffer-and-file-setup)
+ (unwind-protect
+ (let* ((test-dir (cj/create-test-subdirectory "test"))
+ (test-file (expand-file-name "test.txt" test-dir))
+ (delete-file-called nil))
+ (with-temp-file test-file
+ (insert "content"))
+ (find-file test-file)
+ (cl-letf (((symbol-function 'vc-backend) (lambda (&rest _) nil))
+ ((symbol-function 'delete-file)
+ (lambda (file trash)
+ (setq delete-file-called file))))
+ (cj/delete-buffer-and-file)
+ (should (string= delete-file-called test-file))))
+ (test-delete-buffer-and-file-teardown)))
+
+(ert-deftest test-delete-buffer-and-file-returns-implicitly ()
+ "Should return result of last expression."
+ (test-delete-buffer-and-file-setup)
+ (unwind-protect
+ (let* ((test-dir (cj/create-test-subdirectory "test"))
+ (test-file (expand-file-name "test.txt" test-dir)))
+ (with-temp-file test-file
+ (insert "content"))
+ (find-file test-file)
+ (cl-letf (((symbol-function 'vc-backend) (lambda (&rest _) nil)))
+ (let ((result (cj/delete-buffer-and-file)))
+ ;; kill-buffer returns t, so result should be t
+ (should (eq result t)))))
+ (test-delete-buffer-and-file-teardown)))
+
+;;; Boundary Cases - File Content
+
+(ert-deftest test-delete-buffer-and-file-empty-file ()
+ "Should delete empty file."
+ (test-delete-buffer-and-file-setup)
+ (unwind-protect
+ (let* ((test-dir (cj/create-test-subdirectory "test"))
+ (test-file (expand-file-name "empty.txt" test-dir)))
+ (with-temp-file test-file)
+ (find-file test-file)
+ (cl-letf (((symbol-function 'vc-backend) (lambda (&rest _) nil)))
+ (cj/delete-buffer-and-file)
+ (should-not (file-exists-p test-file))))
+ (test-delete-buffer-and-file-teardown)))
+
+(ert-deftest test-delete-buffer-and-file-large-file ()
+ "Should delete large file."
+ (test-delete-buffer-and-file-setup)
+ (unwind-protect
+ (let* ((test-dir (cj/create-test-subdirectory "test"))
+ (test-file (expand-file-name "large.txt" test-dir))
+ (large-content (make-string 100000 ?x)))
+ (with-temp-file test-file
+ (insert large-content))
+ (find-file test-file)
+ (cl-letf (((symbol-function 'vc-backend) (lambda (&rest _) nil)))
+ (cj/delete-buffer-and-file)
+ (should-not (file-exists-p test-file))))
+ (test-delete-buffer-and-file-teardown)))
+
+(ert-deftest test-delete-buffer-and-file-binary-file ()
+ "Should delete binary file."
+ (test-delete-buffer-and-file-setup)
+ (unwind-protect
+ (let* ((test-dir (cj/create-test-subdirectory "test"))
+ (test-file (expand-file-name "binary.dat" test-dir))
+ (binary-content (string 0 1 2 3 255 254 253)))
+ (with-temp-file test-file
+ (set-buffer-multibyte nil)
+ (insert binary-content))
+ (find-file test-file)
+ (cl-letf (((symbol-function 'vc-backend) (lambda (&rest _) nil)))
+ (cj/delete-buffer-and-file)
+ (should-not (file-exists-p test-file))))
+ (test-delete-buffer-and-file-teardown)))
+
+(ert-deftest test-delete-buffer-and-file-with-unicode-content ()
+ "Should delete file with Unicode content."
+ (test-delete-buffer-and-file-setup)
+ (unwind-protect
+ (let* ((test-dir (cj/create-test-subdirectory "test"))
+ (test-file (expand-file-name "unicode.txt" test-dir))
+ (content "Hello 世界 مرحبا Привет"))
+ (with-temp-file test-file
+ (insert content))
+ (find-file test-file)
+ (cl-letf (((symbol-function 'vc-backend) (lambda (&rest _) nil)))
+ (cj/delete-buffer-and-file)
+ (should-not (file-exists-p test-file))))
+ (test-delete-buffer-and-file-teardown)))
+
+;;; Boundary Cases - File Naming
+
+(ert-deftest test-delete-buffer-and-file-unicode-filename ()
+ "Should delete file with Unicode filename."
+ (test-delete-buffer-and-file-setup)
+ (unwind-protect
+ (let* ((test-dir (cj/create-test-subdirectory "test"))
+ (test-file (expand-file-name "café.txt" test-dir)))
+ (with-temp-file test-file
+ (insert "content"))
+ (find-file test-file)
+ (cl-letf (((symbol-function 'vc-backend) (lambda (&rest _) nil)))
+ (cj/delete-buffer-and-file)
+ (should-not (file-exists-p test-file))))
+ (test-delete-buffer-and-file-teardown)))
+
+(ert-deftest test-delete-buffer-and-file-spaces-in-filename ()
+ "Should delete file with spaces in name."
+ (test-delete-buffer-and-file-setup)
+ (unwind-protect
+ (let* ((test-dir (cj/create-test-subdirectory "test"))
+ (test-file (expand-file-name "my file.txt" test-dir)))
+ (with-temp-file test-file
+ (insert "content"))
+ (find-file test-file)
+ (cl-letf (((symbol-function 'vc-backend) (lambda (&rest _) nil)))
+ (cj/delete-buffer-and-file)
+ (should-not (file-exists-p test-file))))
+ (test-delete-buffer-and-file-teardown)))
+
+(ert-deftest test-delete-buffer-and-file-special-chars-filename ()
+ "Should delete file with special characters."
+ (test-delete-buffer-and-file-setup)
+ (unwind-protect
+ (let* ((test-dir (cj/create-test-subdirectory "test"))
+ (test-file (expand-file-name "[test]-(1).txt" test-dir)))
+ (with-temp-file test-file
+ (insert "content"))
+ (find-file test-file)
+ (cl-letf (((symbol-function 'vc-backend) (lambda (&rest _) nil)))
+ (cj/delete-buffer-and-file)
+ (should-not (file-exists-p test-file))))
+ (test-delete-buffer-and-file-teardown)))
+
+(ert-deftest test-delete-buffer-and-file-hidden-file ()
+ "Should delete hidden file."
+ (test-delete-buffer-and-file-setup)
+ (unwind-protect
+ (let* ((test-dir (cj/create-test-subdirectory "test"))
+ (test-file (expand-file-name ".hidden" test-dir)))
+ (with-temp-file test-file
+ (insert "content"))
+ (find-file test-file)
+ (cl-letf (((symbol-function 'vc-backend) (lambda (&rest _) nil)))
+ (cj/delete-buffer-and-file)
+ (should-not (file-exists-p test-file))))
+ (test-delete-buffer-and-file-teardown)))
+
+(ert-deftest test-delete-buffer-and-file-no-extension ()
+ "Should delete file without extension."
+ (test-delete-buffer-and-file-setup)
+ (unwind-protect
+ (let* ((test-dir (cj/create-test-subdirectory "test"))
+ (test-file (expand-file-name "README" test-dir)))
+ (with-temp-file test-file
+ (insert "content"))
+ (find-file test-file)
+ (cl-letf (((symbol-function 'vc-backend) (lambda (&rest _) nil)))
+ (cj/delete-buffer-and-file)
+ (should-not (file-exists-p test-file))))
+ (test-delete-buffer-and-file-teardown)))
+
+(ert-deftest test-delete-buffer-and-file-very-long-filename ()
+ "Should delete file with very long name."
+ (test-delete-buffer-and-file-setup)
+ (unwind-protect
+ (let* ((test-dir (cj/create-test-subdirectory "test"))
+ (long-name (concat (make-string 200 ?x) ".txt"))
+ (test-file (expand-file-name long-name test-dir)))
+ (with-temp-file test-file
+ (insert "content"))
+ (find-file test-file)
+ (cl-letf (((symbol-function 'vc-backend) (lambda (&rest _) nil)))
+ (cj/delete-buffer-and-file)
+ (should-not (file-exists-p test-file))))
+ (test-delete-buffer-and-file-teardown)))
+
+;;; Boundary Cases - Buffer State
+
+(ert-deftest test-delete-buffer-and-file-with-unsaved-changes ()
+ "Should handle buffer with unsaved changes."
+ (test-delete-buffer-and-file-setup)
+ (unwind-protect
+ (let* ((test-dir (cj/create-test-subdirectory "test"))
+ (test-file (expand-file-name "test.txt" test-dir)))
+ (with-temp-file test-file
+ (insert "original"))
+ (find-file test-file)
+ (insert " modified")
+ (should (buffer-modified-p))
+ (let ((buf (current-buffer)))
+ (cl-letf (((symbol-function 'vc-backend) (lambda (&rest _) nil)))
+ (cj/delete-buffer-and-file)
+ (should-not (file-exists-p test-file))
+ (should-not (buffer-live-p buf)))))
+ (test-delete-buffer-and-file-teardown)))
+
+(ert-deftest test-delete-buffer-and-file-read-only-buffer ()
+ "Should handle read-only buffer."
+ (test-delete-buffer-and-file-setup)
+ (unwind-protect
+ (let* ((test-dir (cj/create-test-subdirectory "test"))
+ (test-file (expand-file-name "test.txt" test-dir)))
+ (with-temp-file test-file
+ (insert "content"))
+ (find-file test-file)
+ (read-only-mode 1)
+ (let ((buf (current-buffer)))
+ (cl-letf (((symbol-function 'vc-backend) (lambda (&rest _) nil)))
+ (cj/delete-buffer-and-file)
+ (should-not (file-exists-p test-file))
+ (should-not (buffer-live-p buf)))))
+ (test-delete-buffer-and-file-teardown)))
+
+(ert-deftest test-delete-buffer-and-file-multiple-windows ()
+ "Should work when buffer displayed in multiple windows."
+ (test-delete-buffer-and-file-setup)
+ (unwind-protect
+ (let* ((test-dir (cj/create-test-subdirectory "test"))
+ (test-file (expand-file-name "test.txt" test-dir)))
+ (with-temp-file test-file
+ (insert "content"))
+ (find-file test-file)
+ (delete-other-windows)
+ (split-window)
+ (other-window 1)
+ (switch-to-buffer (get-file-buffer test-file))
+ (let ((buf (current-buffer)))
+ (cl-letf (((symbol-function 'vc-backend) (lambda (&rest _) nil)))
+ (cj/delete-buffer-and-file)
+ (should-not (file-exists-p test-file))
+ (should-not (buffer-live-p buf))))
+ (delete-other-windows))
+ (test-delete-buffer-and-file-teardown)))
+
+(ert-deftest test-delete-buffer-and-file-buffer-not-current ()
+ "Should only operate on current buffer."
+ (test-delete-buffer-and-file-setup)
+ (unwind-protect
+ (let* ((test-dir (cj/create-test-subdirectory "test"))
+ (file1 (expand-file-name "file1.txt" test-dir))
+ (file2 (expand-file-name "file2.txt" test-dir)))
+ (with-temp-file file1
+ (insert "content1"))
+ (with-temp-file file2
+ (insert "content2"))
+ (find-file file1)
+ (find-file file2)
+ ;; Current buffer is file2
+ (cl-letf (((symbol-function 'vc-backend) (lambda (&rest _) nil)))
+ (cj/delete-buffer-and-file)
+ ;; file2 should be deleted, file1 should still exist
+ (should-not (file-exists-p file2))
+ (should (file-exists-p file1)))
+ (kill-buffer (get-file-buffer file1)))
+ (test-delete-buffer-and-file-teardown)))
+
+(ert-deftest test-delete-buffer-and-file-narrowed-buffer ()
+ "Should work with narrowed buffer."
+ (test-delete-buffer-and-file-setup)
+ (unwind-protect
+ (let* ((test-dir (cj/create-test-subdirectory "test"))
+ (test-file (expand-file-name "test.txt" test-dir)))
+ (with-temp-file test-file
+ (insert "Line 1\nLine 2\nLine 3"))
+ (find-file test-file)
+ (goto-char (point-min))
+ (forward-line 1)
+ (narrow-to-region (point) (line-end-position))
+ (let ((buf (current-buffer)))
+ (cl-letf (((symbol-function 'vc-backend) (lambda (&rest _) nil)))
+ (cj/delete-buffer-and-file)
+ (should-not (file-exists-p test-file))
+ (should-not (buffer-live-p buf)))))
+ (test-delete-buffer-and-file-teardown)))
+
+;;; Error Cases - Buffer Issues
+
+(ert-deftest test-delete-buffer-and-file-non-file-buffer-does-nothing ()
+ "Should do nothing if buffer not visiting file."
+ (test-delete-buffer-and-file-setup)
+ (unwind-protect
+ (with-temp-buffer
+ (rename-buffer "non-file-buffer" t)
+ (let ((buf (current-buffer)))
+ (cj/delete-buffer-and-file)
+ ;; Buffer should still be alive
+ (should (buffer-live-p buf))))
+ (test-delete-buffer-and-file-teardown)))
+
+(ert-deftest test-delete-buffer-and-file-scratch-buffer-does-nothing ()
+ "Should do nothing for scratch buffer."
+ (test-delete-buffer-and-file-setup)
+ (unwind-protect
+ (with-current-buffer "*scratch*"
+ (cj/delete-buffer-and-file)
+ ;; Scratch buffer should still exist
+ (should (get-buffer "*scratch*")))
+ (test-delete-buffer-and-file-teardown)))
+
+(ert-deftest test-delete-buffer-and-file-already-killed-buffer ()
+ "Should error when operating on killed buffer."
+ (test-delete-buffer-and-file-setup)
+ (unwind-protect
+ (let* ((test-dir (cj/create-test-subdirectory "test"))
+ (test-file (expand-file-name "test.txt" test-dir))
+ (buf nil))
+ (with-temp-file test-file
+ (insert "content"))
+ (setq buf (find-file test-file))
+ (kill-buffer buf)
+ (should-error
+ (with-current-buffer buf
+ (cj/delete-buffer-and-file))))
+ (test-delete-buffer-and-file-teardown)))
+
+;;; Error Cases - File Issues
+
+(ert-deftest test-delete-buffer-and-file-already-deleted-file ()
+ "Should propagate error when delete-file fails on missing file."
+ (test-delete-buffer-and-file-setup)
+ (unwind-protect
+ (let* ((test-dir (cj/create-test-subdirectory "test"))
+ (test-file (expand-file-name "test.txt" test-dir)))
+ (with-temp-file test-file
+ (insert "content"))
+ (find-file test-file)
+ (cl-letf (((symbol-function 'vc-backend) (lambda (&rest _) nil))
+ ((symbol-function 'delete-file)
+ (lambda (file &optional _trash)
+ (signal 'file-missing (list "Removing old name" "No such file or directory" file)))))
+ ;; Should propagate error from delete-file
+ (should-error (cj/delete-buffer-and-file) :type 'file-missing)))
+ (test-delete-buffer-and-file-teardown)))
+
+(ert-deftest test-delete-buffer-and-file-no-delete-permission ()
+ "Should propagate error when delete-file fails due to permissions."
+ (test-delete-buffer-and-file-setup)
+ (unwind-protect
+ (let* ((test-dir (cj/create-test-subdirectory "test"))
+ (test-file (expand-file-name "test.txt" test-dir)))
+ (with-temp-file test-file
+ (insert "content"))
+ (find-file test-file)
+ (cl-letf (((symbol-function 'vc-backend) (lambda (&rest _) nil))
+ ((symbol-function 'delete-file)
+ (lambda (file &optional _trash)
+ (signal 'file-error (list "Removing old name" "Permission denied" file)))))
+ ;; Should propagate error from delete-file
+ (should-error (cj/delete-buffer-and-file) :type 'file-error)))
+ (test-delete-buffer-and-file-teardown)))
+
+(ert-deftest test-delete-buffer-and-file-no-write-permission-directory ()
+ "Should error if directory not writable."
+ (test-delete-buffer-and-file-setup)
+ (unwind-protect
+ (let* ((test-dir (cj/create-test-subdirectory "test"))
+ (test-file (expand-file-name "test.txt" test-dir)))
+ (with-temp-file test-file
+ (insert "content"))
+ (find-file test-file)
+ (set-file-modes test-dir #o555)
+ (cl-letf (((symbol-function 'vc-backend) (lambda (&rest _) nil)))
+ (should-error (cj/delete-buffer-and-file))
+ (set-file-modes test-dir #o755)))
+ (test-delete-buffer-and-file-teardown)))
+
+(ert-deftest test-delete-buffer-and-file-open-in-other-buffer ()
+ "Should handle file open in another buffer."
+ (test-delete-buffer-and-file-setup)
+ (unwind-protect
+ (let* ((test-dir (cj/create-test-subdirectory "test"))
+ (test-file (expand-file-name "test.txt" test-dir)))
+ (with-temp-file test-file
+ (insert "content"))
+ (find-file test-file)
+ (let ((buf1 (current-buffer)))
+ (find-file test-file)
+ (let ((buf2 (current-buffer)))
+ ;; Both buffers visiting same file
+ (should (eq buf1 buf2))
+ (cl-letf (((symbol-function 'vc-backend) (lambda (&rest _) nil)))
+ (cj/delete-buffer-and-file)
+ (should-not (file-exists-p test-file))
+ (should-not (buffer-live-p buf1))))))
+ (test-delete-buffer-and-file-teardown)))
+
+(ert-deftest test-delete-buffer-and-file-symlink-file ()
+ "Should handle symlink files."
+ (test-delete-buffer-and-file-setup)
+ (unwind-protect
+ (let* ((test-dir (cj/create-test-subdirectory "test"))
+ (real-file (expand-file-name "real.txt" test-dir))
+ (symlink (expand-file-name "link.txt" test-dir)))
+ (with-temp-file real-file
+ (insert "content"))
+ (make-symbolic-link real-file symlink)
+ (find-file symlink)
+ (cl-letf (((symbol-function 'vc-backend) (lambda (&rest _) nil)))
+ (cj/delete-buffer-and-file)
+ ;; Symlink should be deleted, real file should remain
+ (should-not (file-exists-p symlink))
+ (should (file-exists-p real-file))))
+ (test-delete-buffer-and-file-teardown)))
+
+(ert-deftest test-delete-buffer-and-file-symlink-directory ()
+ "Should handle files in symlinked directories."
+ (test-delete-buffer-and-file-setup)
+ (unwind-protect
+ (let* ((real-dir (cj/create-test-subdirectory "real"))
+ (link-dir (expand-file-name "link" cj/test-base-dir))
+ (test-file (expand-file-name "test.txt" real-dir)))
+ (with-temp-file test-file
+ (insert "content"))
+ (make-symbolic-link real-dir link-dir)
+ (let ((file-via-link (expand-file-name "test.txt" link-dir)))
+ (find-file file-via-link)
+ (cl-letf (((symbol-function 'vc-backend) (lambda (&rest _) nil)))
+ (cj/delete-buffer-and-file)
+ ;; File should be deleted
+ (should-not (file-exists-p test-file)))))
+ (test-delete-buffer-and-file-teardown)))
+
+;;; Edge Cases - Version Control
+
+(ert-deftest test-delete-buffer-and-file-git-tracked-file ()
+ "Should use vc-delete-file for git files."
+ (test-delete-buffer-and-file-setup)
+ (unwind-protect
+ (let* ((test-dir (cj/create-test-subdirectory "test"))
+ (test-file (expand-file-name "test.txt" test-dir))
+ (vc-delete-called nil))
+ (with-temp-file test-file
+ (insert "content"))
+ (find-file test-file)
+ (cl-letf (((symbol-function 'vc-backend) (lambda (&rest _) 'Git))
+ ((symbol-function 'vc-delete-file)
+ (lambda (file)
+ (setq vc-delete-called t)
+ (when (get-file-buffer file)
+ (kill-buffer (get-file-buffer file)))
+ (delete-file file t))))
+ (cj/delete-buffer-and-file)
+ (should vc-delete-called)))
+ (test-delete-buffer-and-file-teardown)))
+
+(ert-deftest test-delete-buffer-and-file-untracked-in-vc-repo ()
+ "Should use delete-file for untracked files in VC repo."
+ (test-delete-buffer-and-file-setup)
+ (unwind-protect
+ (let* ((test-dir (cj/create-test-subdirectory "test"))
+ (test-file (expand-file-name "untracked.txt" test-dir)))
+ (with-temp-file test-file
+ (insert "content"))
+ (find-file test-file)
+ ;; vc-backend returns nil for untracked files
+ (cl-letf (((symbol-function 'vc-backend) (lambda (&rest _) nil)))
+ (cj/delete-buffer-and-file)
+ (should-not (file-exists-p test-file))))
+ (test-delete-buffer-and-file-teardown)))
+
+(ert-deftest test-delete-buffer-and-file-vc-backend-detection ()
+ "Should correctly detect VC backend."
+ (test-delete-buffer-and-file-setup)
+ (unwind-protect
+ (let* ((test-dir (cj/create-test-subdirectory "test"))
+ (test-file (expand-file-name "test.txt" test-dir))
+ (backend-checked nil))
+ (with-temp-file test-file
+ (insert "content"))
+ (find-file test-file)
+ (cl-letf (((symbol-function 'vc-backend)
+ (lambda (file)
+ (setq backend-checked file)
+ nil)))
+ (cj/delete-buffer-and-file)
+ (should (string= backend-checked test-file))))
+ (test-delete-buffer-and-file-teardown)))
+
+(ert-deftest test-delete-buffer-and-file-vc-delete-fails ()
+ "Should propagate vc-delete-file errors."
+ (test-delete-buffer-and-file-setup)
+ (unwind-protect
+ (let* ((test-dir (cj/create-test-subdirectory "test"))
+ (test-file (expand-file-name "test.txt" test-dir)))
+ (with-temp-file test-file
+ (insert "content"))
+ (find-file test-file)
+ (cl-letf (((symbol-function 'vc-backend) (lambda (&rest _) 'Git))
+ ((symbol-function 'vc-delete-file)
+ (lambda (file)
+ (error "VC operation failed"))))
+ (should-error (cj/delete-buffer-and-file))))
+ (test-delete-buffer-and-file-teardown)))
+
+(provide 'test-custom-file-buffer-delete-buffer-and-file)
+;;; test-custom-file-buffer-delete-buffer-and-file.el ends here
diff --git a/tests/test-custom-file-buffer-move-buffer-and-file.el b/tests/test-custom-file-buffer-move-buffer-and-file.el
new file mode 100644
index 00000000..1fc16011
--- /dev/null
+++ b/tests/test-custom-file-buffer-move-buffer-and-file.el
@@ -0,0 +1,936 @@
+;;; test-custom-file-buffer-move-buffer-and-file.el --- Tests for cj/move-buffer-and-file -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; Tests for the cj/--move-buffer-and-file function from custom-file-buffer.el
+;;
+;; This is the internal (non-interactive) implementation that moves both the
+;; current buffer and its visited file to a new directory. It handles trailing
+;; slashes, preserves file content, updates the visited-file-name, and clears
+;; the modified flag. The interactive wrapper cj/move-buffer-and-file handles
+;; user prompting and delegates to this implementation.
+
+;;; Code:
+
+(require 'ert)
+(require 'testutil-general)
+
+;; Add modules directory to load path
+(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory))
+
+;; Stub dependencies before loading the module
+(defvar cj/custom-keymap (make-sparse-keymap)
+ "Stub keymap for testing.")
+
+;; Stub ps-print package
+(provide 'ps-print)
+
+;; Now load the actual production module
+(require 'custom-file-buffer)
+
+;;; Setup and Teardown
+
+(defun test-move-buffer-and-file-setup ()
+ "Setup for move-buffer-and-file tests."
+ (cj/create-test-base-dir))
+
+(defun test-move-buffer-and-file-teardown ()
+ "Teardown for move-buffer-and-file tests."
+ ;; Kill all buffers visiting files in test directory
+ (dolist (buf (buffer-list))
+ (when (buffer-file-name buf)
+ (when (string-prefix-p cj/test-base-dir (buffer-file-name buf))
+ (with-current-buffer buf
+ (set-buffer-modified-p nil))
+ (kill-buffer buf))))
+ (cj/delete-test-base-dir))
+
+;;; Normal Cases
+
+(ert-deftest test-move-buffer-and-file-simple-move-should-succeed ()
+ "Should move file and buffer to new directory successfully."
+ (test-move-buffer-and-file-setup)
+ (unwind-protect
+ (let* ((source-dir (cj/create-test-subdirectory "source"))
+ (target-dir (cj/create-test-subdirectory "target"))
+ (source-file (expand-file-name "test.txt" source-dir))
+ (target-file (expand-file-name "test.txt" target-dir))
+ (content "Test content"))
+ (with-temp-file source-file
+ (insert content))
+ (find-file source-file)
+ (cj/--move-buffer-and-file target-dir)
+ (should (file-exists-p target-file))
+ (kill-buffer (current-buffer)))
+ (test-move-buffer-and-file-teardown)))
+
+(ert-deftest test-move-buffer-and-file-updates-buffer-file-name ()
+ "Should update buffer-file-name to new location."
+ (test-move-buffer-and-file-setup)
+ (unwind-protect
+ (let* ((source-dir (cj/create-test-subdirectory "source"))
+ (target-dir (cj/create-test-subdirectory "target"))
+ (source-file (expand-file-name "test.txt" source-dir))
+ (target-file (expand-file-name "test.txt" target-dir)))
+ (with-temp-file source-file
+ (insert "content"))
+ (find-file source-file)
+ (cj/--move-buffer-and-file target-dir)
+ (should (string= (buffer-file-name) target-file))
+ (kill-buffer (current-buffer)))
+ (test-move-buffer-and-file-teardown)))
+
+(ert-deftest test-move-buffer-and-file-preserves-content ()
+ "Should preserve file content after move."
+ (test-move-buffer-and-file-setup)
+ (unwind-protect
+ (let* ((source-dir (cj/create-test-subdirectory "source"))
+ (target-dir (cj/create-test-subdirectory "target"))
+ (source-file (expand-file-name "test.txt" source-dir))
+ (content "Original content\nWith multiple lines\n"))
+ (with-temp-file source-file
+ (insert content))
+ (find-file source-file)
+ (cj/--move-buffer-and-file target-dir)
+ (should (string= (buffer-string) content))
+ (kill-buffer (current-buffer)))
+ (test-move-buffer-and-file-teardown)))
+
+(ert-deftest test-move-buffer-and-file-preserves-buffer-name ()
+ "Should preserve buffer name after move."
+ (test-move-buffer-and-file-setup)
+ (unwind-protect
+ (let* ((source-dir (cj/create-test-subdirectory "source"))
+ (target-dir (cj/create-test-subdirectory "target"))
+ (source-file (expand-file-name "myfile.txt" source-dir)))
+ (with-temp-file source-file
+ (insert "content"))
+ (find-file source-file)
+ (should (string= (buffer-name) "myfile.txt"))
+ (cj/--move-buffer-and-file target-dir)
+ (should (string= (buffer-name) "myfile.txt"))
+ (kill-buffer (current-buffer)))
+ (test-move-buffer-and-file-teardown)))
+
+(ert-deftest test-move-buffer-and-file-clears-modified-flag ()
+ "Should clear buffer modified flag after move."
+ (test-move-buffer-and-file-setup)
+ (unwind-protect
+ (let* ((source-dir (cj/create-test-subdirectory "source"))
+ (target-dir (cj/create-test-subdirectory "target"))
+ (source-file (expand-file-name "test.txt" source-dir)))
+ (with-temp-file source-file
+ (insert "content"))
+ (find-file source-file)
+ (insert "modification")
+ (should (buffer-modified-p))
+ (cj/--move-buffer-and-file target-dir)
+ (should-not (buffer-modified-p))
+ (kill-buffer (current-buffer)))
+ (test-move-buffer-and-file-teardown)))
+
+(ert-deftest test-move-buffer-and-file-returns-t-on-success ()
+ "Should return t on successful move."
+ (test-move-buffer-and-file-setup)
+ (unwind-protect
+ (let* ((source-dir (cj/create-test-subdirectory "source"))
+ (target-dir (cj/create-test-subdirectory "target"))
+ (source-file (expand-file-name "test.txt" source-dir)))
+ (with-temp-file source-file
+ (insert "content"))
+ (find-file source-file)
+ (should (eq t (cj/--move-buffer-and-file target-dir)))
+ (kill-buffer (current-buffer)))
+ (test-move-buffer-and-file-teardown)))
+
+(ert-deftest test-move-buffer-and-file-deletes-source-file ()
+ "Should delete source file after move."
+ (test-move-buffer-and-file-setup)
+ (unwind-protect
+ (let* ((source-dir (cj/create-test-subdirectory "source"))
+ (target-dir (cj/create-test-subdirectory "target"))
+ (source-file (expand-file-name "test.txt" source-dir)))
+ (with-temp-file source-file
+ (insert "content"))
+ (find-file source-file)
+ (cj/--move-buffer-and-file target-dir)
+ (should-not (file-exists-p source-file))
+ (kill-buffer (current-buffer)))
+ (test-move-buffer-and-file-teardown)))
+
+(ert-deftest test-move-buffer-and-file-creates-target-file ()
+ "Should create file in target directory."
+ (test-move-buffer-and-file-setup)
+ (unwind-protect
+ (let* ((source-dir (cj/create-test-subdirectory "source"))
+ (target-dir (cj/create-test-subdirectory "target"))
+ (source-file (expand-file-name "test.txt" source-dir))
+ (target-file (expand-file-name "test.txt" target-dir)))
+ (with-temp-file source-file
+ (insert "content"))
+ (find-file source-file)
+ (cj/--move-buffer-and-file target-dir)
+ (should (file-exists-p target-file))
+ (kill-buffer (current-buffer)))
+ (test-move-buffer-and-file-teardown)))
+
+;;; Boundary Cases - Path Handling
+
+(ert-deftest test-move-buffer-and-file-trailing-slash-should-strip ()
+ "Should handle directory with trailing slash."
+ (test-move-buffer-and-file-setup)
+ (unwind-protect
+ (let* ((source-dir (cj/create-test-subdirectory "source"))
+ (target-dir (cj/create-test-subdirectory "target"))
+ (source-file (expand-file-name "test.txt" source-dir))
+ (target-file (expand-file-name "test.txt" target-dir)))
+ (with-temp-file source-file
+ (insert "content"))
+ (find-file source-file)
+ (cj/--move-buffer-and-file (concat target-dir "/"))
+ (should (file-exists-p target-file))
+ (should-not (file-exists-p source-file))
+ (kill-buffer (current-buffer)))
+ (test-move-buffer-and-file-teardown)))
+
+(ert-deftest test-move-buffer-and-file-trailing-backslash-should-strip ()
+ "Should handle directory with trailing backslash."
+ (test-move-buffer-and-file-setup)
+ (unwind-protect
+ (let* ((source-dir (cj/create-test-subdirectory "source"))
+ (target-dir (cj/create-test-subdirectory "target"))
+ (source-file (expand-file-name "test.txt" source-dir))
+ (target-file (expand-file-name "test.txt" target-dir)))
+ (with-temp-file source-file
+ (insert "content"))
+ (find-file source-file)
+ (cj/--move-buffer-and-file (concat target-dir "\\"))
+ (should (file-exists-p target-file))
+ (should-not (file-exists-p source-file))
+ (kill-buffer (current-buffer)))
+ (test-move-buffer-and-file-teardown)))
+
+(ert-deftest test-move-buffer-and-file-no-trailing-slash-should-work ()
+ "Should work with directory without trailing slash."
+ (test-move-buffer-and-file-setup)
+ (unwind-protect
+ (let* ((source-dir (cj/create-test-subdirectory "source"))
+ (target-dir (cj/create-test-subdirectory "target"))
+ (source-file (expand-file-name "test.txt" source-dir))
+ (target-file (expand-file-name "test.txt" target-dir)))
+ (with-temp-file source-file
+ (insert "content"))
+ (find-file source-file)
+ (cj/--move-buffer-and-file target-dir)
+ (should (file-exists-p target-file))
+ (kill-buffer (current-buffer)))
+ (test-move-buffer-and-file-teardown)))
+
+(ert-deftest test-move-buffer-and-file-deeply-nested-target ()
+ "Should move to deeply nested target directory."
+ (test-move-buffer-and-file-setup)
+ (unwind-protect
+ (let* ((source-dir (cj/create-test-subdirectory "source"))
+ (target-dir (cj/create-test-subdirectory "a/b/c/d/target"))
+ (source-file (expand-file-name "test.txt" source-dir))
+ (target-file (expand-file-name "test.txt" target-dir)))
+ (with-temp-file source-file
+ (insert "content"))
+ (find-file source-file)
+ (cj/--move-buffer-and-file target-dir)
+ (should (file-exists-p target-file))
+ (should-not (file-exists-p source-file))
+ (kill-buffer (current-buffer)))
+ (test-move-buffer-and-file-teardown)))
+
+(ert-deftest test-move-buffer-and-file-relative-path-should-work ()
+ "Should resolve relative paths relative to file's directory."
+ (test-move-buffer-and-file-setup)
+ (unwind-protect
+ (let* ((source-dir (cj/create-test-subdirectory "source"))
+ (target-dir (cj/create-test-subdirectory "target"))
+ (source-file (expand-file-name "test.txt" source-dir))
+ (target-file (expand-file-name "test.txt" target-dir)))
+ (with-temp-file source-file
+ (insert "content"))
+ (find-file source-file)
+ ;; Use "../target" to go up from source/ to target/
+ (cj/--move-buffer-and-file "../target")
+ (should (file-exists-p target-file))
+ (should-not (file-exists-p source-file))
+ (kill-buffer (current-buffer)))
+ (test-move-buffer-and-file-teardown)))
+
+;;; Boundary Cases - Character Encoding
+
+(ert-deftest test-move-buffer-and-file-unicode-filename ()
+ "Should handle Unicode characters in filename."
+ (test-move-buffer-and-file-setup)
+ (unwind-protect
+ (let* ((source-dir (cj/create-test-subdirectory "source"))
+ (target-dir (cj/create-test-subdirectory "target"))
+ (source-file (expand-file-name "test-café.txt" source-dir))
+ (target-file (expand-file-name "test-café.txt" target-dir)))
+ (with-temp-file source-file
+ (insert "content"))
+ (find-file source-file)
+ (cj/--move-buffer-and-file target-dir)
+ (should (file-exists-p target-file))
+ (should-not (file-exists-p source-file))
+ (kill-buffer (current-buffer)))
+ (test-move-buffer-and-file-teardown)))
+
+(ert-deftest test-move-buffer-and-file-unicode-directory ()
+ "Should handle Unicode characters in directory name."
+ (test-move-buffer-and-file-setup)
+ (unwind-protect
+ (let* ((source-dir (cj/create-test-subdirectory "source"))
+ (target-dir (cj/create-test-subdirectory "target-ñoño"))
+ (source-file (expand-file-name "test.txt" source-dir))
+ (target-file (expand-file-name "test.txt" target-dir)))
+ (with-temp-file source-file
+ (insert "content"))
+ (find-file source-file)
+ (cj/--move-buffer-and-file target-dir)
+ (should (file-exists-p target-file))
+ (should-not (file-exists-p source-file))
+ (kill-buffer (current-buffer)))
+ (test-move-buffer-and-file-teardown)))
+
+(ert-deftest test-move-buffer-and-file-emoji-in-filename ()
+ "Should handle emoji in filename."
+ (test-move-buffer-and-file-setup)
+ (unwind-protect
+ (let* ((source-dir (cj/create-test-subdirectory "source"))
+ (target-dir (cj/create-test-subdirectory "target"))
+ (source-file (expand-file-name "test-🎉-file.txt" source-dir))
+ (target-file (expand-file-name "test-🎉-file.txt" target-dir)))
+ (with-temp-file source-file
+ (insert "content"))
+ (find-file source-file)
+ (cj/--move-buffer-and-file target-dir)
+ (should (file-exists-p target-file))
+ (should-not (file-exists-p source-file))
+ (kill-buffer (current-buffer)))
+ (test-move-buffer-and-file-teardown)))
+
+(ert-deftest test-move-buffer-and-file-rtl-characters ()
+ "Should handle RTL text in filename."
+ (test-move-buffer-and-file-setup)
+ (unwind-protect
+ (let* ((source-dir (cj/create-test-subdirectory "source"))
+ (target-dir (cj/create-test-subdirectory "target"))
+ (source-file (expand-file-name "test-مرحبا.txt" source-dir))
+ (target-file (expand-file-name "test-مرحبا.txt" target-dir)))
+ (with-temp-file source-file
+ (insert "content"))
+ (find-file source-file)
+ (cj/--move-buffer-and-file target-dir)
+ (should (file-exists-p target-file))
+ (should-not (file-exists-p source-file))
+ (kill-buffer (current-buffer)))
+ (test-move-buffer-and-file-teardown)))
+
+(ert-deftest test-move-buffer-and-file-spaces-in-filename ()
+ "Should handle spaces in filename."
+ (test-move-buffer-and-file-setup)
+ (unwind-protect
+ (let* ((source-dir (cj/create-test-subdirectory "source"))
+ (target-dir (cj/create-test-subdirectory "target"))
+ (source-file (expand-file-name "test file with spaces.txt" source-dir))
+ (target-file (expand-file-name "test file with spaces.txt" target-dir)))
+ (with-temp-file source-file
+ (insert "content"))
+ (find-file source-file)
+ (cj/--move-buffer-and-file target-dir)
+ (should (file-exists-p target-file))
+ (should-not (file-exists-p source-file))
+ (kill-buffer (current-buffer)))
+ (test-move-buffer-and-file-teardown)))
+
+(ert-deftest test-move-buffer-and-file-special-chars-in-filename ()
+ "Should handle special characters in filename."
+ (test-move-buffer-and-file-setup)
+ (unwind-protect
+ (let* ((source-dir (cj/create-test-subdirectory "source"))
+ (target-dir (cj/create-test-subdirectory "target"))
+ (source-file (expand-file-name "test[file]-(1).txt" source-dir))
+ (target-file (expand-file-name "test[file]-(1).txt" target-dir)))
+ (with-temp-file source-file
+ (insert "content"))
+ (find-file source-file)
+ (cj/--move-buffer-and-file target-dir)
+ (should (file-exists-p target-file))
+ (should-not (file-exists-p source-file))
+ (kill-buffer (current-buffer)))
+ (test-move-buffer-and-file-teardown)))
+
+;;; Boundary Cases - File Naming
+
+(ert-deftest test-move-buffer-and-file-hidden-file ()
+ "Should handle hidden files (starting with dot)."
+ (test-move-buffer-and-file-setup)
+ (unwind-protect
+ (let* ((source-dir (cj/create-test-subdirectory "source"))
+ (target-dir (cj/create-test-subdirectory "target"))
+ (source-file (expand-file-name ".hidden" source-dir))
+ (target-file (expand-file-name ".hidden" target-dir)))
+ (with-temp-file source-file
+ (insert "content"))
+ (find-file source-file)
+ (cj/--move-buffer-and-file target-dir)
+ (should (file-exists-p target-file))
+ (should-not (file-exists-p source-file))
+ (kill-buffer (current-buffer)))
+ (test-move-buffer-and-file-teardown)))
+
+(ert-deftest test-move-buffer-and-file-no-extension ()
+ "Should handle files without extensions."
+ (test-move-buffer-and-file-setup)
+ (unwind-protect
+ (let* ((source-dir (cj/create-test-subdirectory "source"))
+ (target-dir (cj/create-test-subdirectory "target"))
+ (source-file (expand-file-name "README" source-dir))
+ (target-file (expand-file-name "README" target-dir)))
+ (with-temp-file source-file
+ (insert "content"))
+ (find-file source-file)
+ (cj/--move-buffer-and-file target-dir)
+ (should (file-exists-p target-file))
+ (should-not (file-exists-p source-file))
+ (kill-buffer (current-buffer)))
+ (test-move-buffer-and-file-teardown)))
+
+(ert-deftest test-move-buffer-and-file-multiple-dots-in-name ()
+ "Should handle multiple dots in filename."
+ (test-move-buffer-and-file-setup)
+ (unwind-protect
+ (let* ((source-dir (cj/create-test-subdirectory "source"))
+ (target-dir (cj/create-test-subdirectory "target"))
+ (source-file (expand-file-name "my.file.name.test.txt" source-dir))
+ (target-file (expand-file-name "my.file.name.test.txt" target-dir)))
+ (with-temp-file source-file
+ (insert "content"))
+ (find-file source-file)
+ (cj/--move-buffer-and-file target-dir)
+ (should (file-exists-p target-file))
+ (should-not (file-exists-p source-file))
+ (kill-buffer (current-buffer)))
+ (test-move-buffer-and-file-teardown)))
+
+(ert-deftest test-move-buffer-and-file-single-char-filename ()
+ "Should handle single character filenames."
+ (test-move-buffer-and-file-setup)
+ (unwind-protect
+ (let* ((source-dir (cj/create-test-subdirectory "source"))
+ (target-dir (cj/create-test-subdirectory "target"))
+ (source-file (expand-file-name "x" source-dir))
+ (target-file (expand-file-name "x" target-dir)))
+ (with-temp-file source-file
+ (insert "content"))
+ (find-file source-file)
+ (cj/--move-buffer-and-file target-dir)
+ (should (file-exists-p target-file))
+ (should-not (file-exists-p source-file))
+ (kill-buffer (current-buffer)))
+ (test-move-buffer-and-file-teardown)))
+
+(ert-deftest test-move-buffer-and-file-very-long-filename ()
+ "Should handle very long filenames."
+ (test-move-buffer-and-file-setup)
+ (unwind-protect
+ (let* ((source-dir (cj/create-test-subdirectory "source"))
+ (target-dir (cj/create-test-subdirectory "target"))
+ (long-name (concat (make-string 200 ?x) ".txt"))
+ (source-file (expand-file-name long-name source-dir))
+ (target-file (expand-file-name long-name target-dir)))
+ (with-temp-file source-file
+ (insert "content"))
+ (find-file source-file)
+ (cj/--move-buffer-and-file target-dir)
+ (should (file-exists-p target-file))
+ (should-not (file-exists-p source-file))
+ (kill-buffer (current-buffer)))
+ (test-move-buffer-and-file-teardown)))
+
+(ert-deftest test-move-buffer-and-file-very-long-path ()
+ "Should handle very long paths."
+ (test-move-buffer-and-file-setup)
+ (unwind-protect
+ (let* ((long-dir (make-string 100 ?x))
+ (source-dir (cj/create-test-subdirectory "source"))
+ (target-dir (cj/create-test-subdirectory long-dir))
+ (long-filename (concat (make-string 100 ?y) ".txt"))
+ (source-file (expand-file-name long-filename source-dir))
+ (target-file (expand-file-name long-filename target-dir)))
+ (with-temp-file source-file
+ (insert "content"))
+ (find-file source-file)
+ (cj/--move-buffer-and-file target-dir)
+ (should (file-exists-p target-file))
+ (should-not (file-exists-p source-file))
+ (kill-buffer (current-buffer)))
+ (test-move-buffer-and-file-teardown)))
+
+;;; Boundary Cases - File Content
+
+(ert-deftest test-move-buffer-and-file-empty-file ()
+ "Should move empty file successfully."
+ (test-move-buffer-and-file-setup)
+ (unwind-protect
+ (let* ((source-dir (cj/create-test-subdirectory "source"))
+ (target-dir (cj/create-test-subdirectory "target"))
+ (source-file (expand-file-name "empty.txt" source-dir))
+ (target-file (expand-file-name "empty.txt" target-dir)))
+ (with-temp-file source-file)
+ (find-file source-file)
+ (cj/--move-buffer-and-file target-dir)
+ (should (file-exists-p target-file))
+ (should-not (file-exists-p source-file))
+ (should (= 0 (buffer-size)))
+ (kill-buffer (current-buffer)))
+ (test-move-buffer-and-file-teardown)))
+
+(ert-deftest test-move-buffer-and-file-large-file ()
+ "Should move large file successfully."
+ (test-move-buffer-and-file-setup)
+ (unwind-protect
+ (let* ((source-dir (cj/create-test-subdirectory "source"))
+ (target-dir (cj/create-test-subdirectory "target"))
+ (source-file (expand-file-name "large.txt" source-dir))
+ (large-content (make-string 100000 ?x)))
+ (with-temp-file source-file
+ (insert large-content))
+ (find-file source-file)
+ (cj/--move-buffer-and-file target-dir)
+ (should (string= (buffer-string) large-content))
+ (kill-buffer (current-buffer)))
+ (test-move-buffer-and-file-teardown)))
+
+(ert-deftest test-move-buffer-and-file-binary-file ()
+ "Should move binary-like content successfully."
+ (test-move-buffer-and-file-setup)
+ (unwind-protect
+ (let* ((source-dir (cj/create-test-subdirectory "source"))
+ (target-dir (cj/create-test-subdirectory "target"))
+ (source-file (expand-file-name "binary.dat" source-dir))
+ (target-file (expand-file-name "binary.dat" target-dir))
+ (binary-content (string 0 1 2 3 255 254 253)))
+ (with-temp-file source-file
+ (set-buffer-multibyte nil)
+ (insert binary-content))
+ (find-file source-file)
+ (cj/--move-buffer-and-file target-dir)
+ (should (file-exists-p target-file))
+ (kill-buffer (current-buffer)))
+ (test-move-buffer-and-file-teardown)))
+
+(ert-deftest test-move-buffer-and-file-preserves-newlines ()
+ "Should preserve different newline types."
+ (test-move-buffer-and-file-setup)
+ (unwind-protect
+ (let* ((source-dir (cj/create-test-subdirectory "source"))
+ (target-dir (cj/create-test-subdirectory "target"))
+ (source-file (expand-file-name "newlines.txt" source-dir))
+ (content "Line 1\nLine 2\n\nLine 4\n"))
+ (with-temp-file source-file
+ (insert content))
+ (find-file source-file)
+ (cj/--move-buffer-and-file target-dir)
+ (should (string= (buffer-string) content))
+ (kill-buffer (current-buffer)))
+ (test-move-buffer-and-file-teardown)))
+
+(ert-deftest test-move-buffer-and-file-preserves-encoding ()
+ "Should preserve UTF-8 encoded content."
+ (test-move-buffer-and-file-setup)
+ (unwind-protect
+ (let* ((source-dir (cj/create-test-subdirectory "source"))
+ (target-dir (cj/create-test-subdirectory "target"))
+ (source-file (expand-file-name "utf8.txt" source-dir))
+ (content "Hello 世界 مرحبا Привет"))
+ (with-temp-file source-file
+ (insert content))
+ (find-file source-file)
+ (cj/--move-buffer-and-file target-dir)
+ (should (string= (buffer-string) content))
+ (kill-buffer (current-buffer)))
+ (test-move-buffer-and-file-teardown)))
+
+;;; Boundary Cases - Buffer State
+
+(ert-deftest test-move-buffer-and-file-with-unsaved-changes ()
+ "Should handle buffer with unsaved changes."
+ (test-move-buffer-and-file-setup)
+ (unwind-protect
+ (let* ((source-dir (cj/create-test-subdirectory "source"))
+ (target-dir (cj/create-test-subdirectory "target"))
+ (source-file (expand-file-name "test.txt" source-dir))
+ (target-file (expand-file-name "test.txt" target-dir))
+ (original "original"))
+ (with-temp-file source-file
+ (insert original))
+ (find-file source-file)
+ (insert " modified")
+ (should (buffer-modified-p))
+ (cj/--move-buffer-and-file target-dir)
+ (should (file-exists-p target-file))
+ (should-not (buffer-modified-p))
+ (kill-buffer (current-buffer)))
+ (test-move-buffer-and-file-teardown)))
+
+(ert-deftest test-move-buffer-and-file-with-multiple-windows ()
+ "Should work when buffer is displayed in multiple windows."
+ (test-move-buffer-and-file-setup)
+ (unwind-protect
+ (let* ((source-dir (cj/create-test-subdirectory "source"))
+ (target-dir (cj/create-test-subdirectory "target"))
+ (source-file (expand-file-name "test.txt" source-dir))
+ (target-file (expand-file-name "test.txt" target-dir)))
+ (with-temp-file source-file
+ (insert "content"))
+ (find-file source-file)
+ (delete-other-windows)
+ (split-window)
+ (other-window 1)
+ (switch-to-buffer (get-file-buffer source-file))
+ (cj/--move-buffer-and-file target-dir)
+ (should (file-exists-p target-file))
+ (kill-buffer (current-buffer))
+ (delete-other-windows))
+ (test-move-buffer-and-file-teardown)))
+
+(ert-deftest test-move-buffer-and-file-preserves-point-position ()
+ "Should preserve point position in buffer."
+ (test-move-buffer-and-file-setup)
+ (unwind-protect
+ (let* ((source-dir (cj/create-test-subdirectory "source"))
+ (target-dir (cj/create-test-subdirectory "target"))
+ (source-file (expand-file-name "test.txt" source-dir))
+ (content "Line 1\nLine 2\nLine 3\n"))
+ (with-temp-file source-file
+ (insert content))
+ (find-file source-file)
+ (goto-char (point-min))
+ (forward-line 1)
+ (let ((original-point (point)))
+ (cj/--move-buffer-and-file target-dir)
+ (should (= (point) original-point)))
+ (kill-buffer (current-buffer)))
+ (test-move-buffer-and-file-teardown)))
+
+(ert-deftest test-move-buffer-and-file-preserves-mark ()
+ "Should preserve mark in buffer."
+ (test-move-buffer-and-file-setup)
+ (unwind-protect
+ (let* ((source-dir (cj/create-test-subdirectory "source"))
+ (target-dir (cj/create-test-subdirectory "target"))
+ (source-file (expand-file-name "test.txt" source-dir))
+ (content "Line 1\nLine 2\nLine 3\n"))
+ (with-temp-file source-file
+ (insert content))
+ (find-file source-file)
+ (goto-char (point-min))
+ (set-mark (point))
+ (forward-line 2)
+ (let ((original-mark (mark)))
+ (cj/--move-buffer-and-file target-dir)
+ (should (= (mark) original-mark)))
+ (kill-buffer (current-buffer)))
+ (test-move-buffer-and-file-teardown)))
+
+;;; Error Cases - Buffer Issues
+
+(ert-deftest test-move-buffer-and-file-non-file-buffer-returns-nil ()
+ "Should return nil when buffer not visiting a file."
+ (test-move-buffer-and-file-setup)
+ (unwind-protect
+ (let ((target-dir (cj/create-test-subdirectory "target")))
+ (with-temp-buffer
+ (rename-buffer "non-file-buffer" t)
+ (let ((result (cj/--move-buffer-and-file target-dir)))
+ (should-not result))))
+ (test-move-buffer-and-file-teardown)))
+
+(ert-deftest test-move-buffer-and-file-scratch-buffer-returns-nil ()
+ "Should return nil for scratch buffer."
+ (test-move-buffer-and-file-setup)
+ (unwind-protect
+ (let ((target-dir (cj/create-test-subdirectory "target")))
+ (with-current-buffer "*scratch*"
+ (let ((result (cj/--move-buffer-and-file target-dir)))
+ (should-not result))))
+ (test-move-buffer-and-file-teardown)))
+
+(ert-deftest test-move-buffer-and-file-killed-buffer-should-error ()
+ "Should error when operating on killed buffer."
+ (test-move-buffer-and-file-setup)
+ (unwind-protect
+ (let* ((source-dir (cj/create-test-subdirectory "source"))
+ (target-dir (cj/create-test-subdirectory "target"))
+ (source-file (expand-file-name "test.txt" source-dir))
+ (buf nil))
+ (with-temp-file source-file
+ (insert "content"))
+ (setq buf (find-file source-file))
+ (kill-buffer buf)
+ (should-error
+ (with-current-buffer buf
+ (cj/--move-buffer-and-file target-dir))))
+ (test-move-buffer-and-file-teardown)))
+
+;;; Error Cases - Directory Issues
+
+(ert-deftest test-move-buffer-and-file-nonexistent-target-should-error ()
+ "Should error when target directory doesn't exist."
+ (test-move-buffer-and-file-setup)
+ (unwind-protect
+ (let* ((source-dir (cj/create-test-subdirectory "source"))
+ (source-file (expand-file-name "test.txt" source-dir))
+ (nonexistent-dir (expand-file-name "nonexistent" cj/test-base-dir)))
+ (with-temp-file source-file
+ (insert "content"))
+ (find-file source-file)
+ (should-error (cj/--move-buffer-and-file nonexistent-dir))
+ (kill-buffer (current-buffer)))
+ (test-move-buffer-and-file-teardown)))
+
+(ert-deftest test-move-buffer-and-file-target-is-file-not-dir-should-error ()
+ "Should error when target is a file, not directory."
+ (test-move-buffer-and-file-setup)
+ (unwind-protect
+ (let* ((source-dir (cj/create-test-subdirectory "source"))
+ (source-file (expand-file-name "test.txt" source-dir))
+ (target-file (expand-file-name "notadir.txt" cj/test-base-dir)))
+ (with-temp-file target-file
+ (insert "I'm a file"))
+ (with-temp-file source-file
+ (insert "content"))
+ (find-file source-file)
+ (should-error (cj/--move-buffer-and-file target-file))
+ (kill-buffer (current-buffer)))
+ (test-move-buffer-and-file-teardown)))
+
+(ert-deftest test-move-buffer-and-file-nil-directory-should-error ()
+ "Should error when directory is nil."
+ (test-move-buffer-and-file-setup)
+ (unwind-protect
+ (let* ((source-dir (cj/create-test-subdirectory "source"))
+ (source-file (expand-file-name "test.txt" source-dir)))
+ (with-temp-file source-file
+ (insert "content"))
+ (find-file source-file)
+ (should-error (cj/--move-buffer-and-file nil))
+ (kill-buffer (current-buffer)))
+ (test-move-buffer-and-file-teardown)))
+
+(ert-deftest test-move-buffer-and-file-empty-string-directory-should-error ()
+ "Should error when directory is empty string."
+ (test-move-buffer-and-file-setup)
+ (unwind-protect
+ (let* ((source-dir (cj/create-test-subdirectory "source"))
+ (source-file (expand-file-name "test.txt" source-dir)))
+ (with-temp-file source-file
+ (insert "content"))
+ (find-file source-file)
+ (should-error (cj/--move-buffer-and-file ""))
+ (kill-buffer (current-buffer)))
+ (test-move-buffer-and-file-teardown)))
+
+;;; Error Cases - Permission Issues
+
+(ert-deftest test-move-buffer-and-file-no-read-permission-source-should-error ()
+ "Should error when source file is not readable."
+ (test-move-buffer-and-file-setup)
+ (unwind-protect
+ (let* ((source-dir (cj/create-test-subdirectory "source"))
+ (target-dir (cj/create-test-subdirectory "target"))
+ (source-file (expand-file-name "test.txt" source-dir)))
+ (with-temp-file source-file
+ (insert "content"))
+ (find-file source-file)
+ (set-file-modes source-file #o000)
+ (should-error (cj/--move-buffer-and-file target-dir))
+ (set-file-modes source-file #o644)
+ (kill-buffer (current-buffer)))
+ (test-move-buffer-and-file-teardown)))
+
+(ert-deftest test-move-buffer-and-file-no-write-permission-target-should-error ()
+ "Should error when target directory is not writable."
+ (test-move-buffer-and-file-setup)
+ (unwind-protect
+ (let* ((source-dir (cj/create-test-subdirectory "source"))
+ (target-dir (cj/create-test-subdirectory "target"))
+ (source-file (expand-file-name "test.txt" source-dir)))
+ (with-temp-file source-file
+ (insert "content"))
+ (set-file-modes target-dir #o555)
+ (find-file source-file)
+ (should-error (cj/--move-buffer-and-file target-dir))
+ (set-file-modes target-dir #o755)
+ (kill-buffer (current-buffer)))
+ (test-move-buffer-and-file-teardown)))
+
+(ert-deftest test-move-buffer-and-file-no-delete-permission-source-should-error ()
+ "Should error when source directory doesn't allow deletion."
+ (test-move-buffer-and-file-setup)
+ (unwind-protect
+ (let* ((source-dir (cj/create-test-subdirectory "source"))
+ (target-dir (cj/create-test-subdirectory "target"))
+ (source-file (expand-file-name "test.txt" source-dir)))
+ (with-temp-file source-file
+ (insert "content"))
+ (find-file source-file)
+ (set-file-modes source-dir #o555)
+ (should-error (cj/--move-buffer-and-file target-dir))
+ (set-file-modes source-dir #o755)
+ (kill-buffer (current-buffer)))
+ (test-move-buffer-and-file-teardown)))
+
+;;; Error Cases - File Conflicts
+
+(ert-deftest test-move-buffer-and-file-target-exists-should-overwrite ()
+ "Should overwrite existing file when ok-if-exists is t."
+ (test-move-buffer-and-file-setup)
+ (unwind-protect
+ (let* ((source-dir (cj/create-test-subdirectory "source"))
+ (target-dir (cj/create-test-subdirectory "target"))
+ (source-file (expand-file-name "test.txt" source-dir))
+ (target-file (expand-file-name "test.txt" target-dir))
+ (new-content "New content")
+ (old-content "Old content"))
+ (with-temp-file target-file
+ (insert old-content))
+ (with-temp-file source-file
+ (insert new-content))
+ (find-file source-file)
+ (cj/--move-buffer-and-file target-dir t)
+ (should (file-exists-p target-file))
+ (should-not (file-exists-p source-file))
+ (revert-buffer t t)
+ (should (string= (buffer-string) new-content))
+ (kill-buffer (current-buffer)))
+ (test-move-buffer-and-file-teardown)))
+
+(ert-deftest test-move-buffer-and-file-target-exists-should-error-if-not-ok ()
+ "Should error when target exists and ok-if-exists is nil."
+ (test-move-buffer-and-file-setup)
+ (unwind-protect
+ (let* ((source-dir (cj/create-test-subdirectory "source"))
+ (target-dir (cj/create-test-subdirectory "target"))
+ (source-file (expand-file-name "test.txt" source-dir))
+ (target-file (expand-file-name "test.txt" target-dir)))
+ (with-temp-file target-file
+ (insert "existing"))
+ (with-temp-file source-file
+ (insert "new"))
+ (find-file source-file)
+ (should-error (cj/--move-buffer-and-file target-dir nil))
+ ;; Source should still exist since move failed
+ (should (file-exists-p source-file))
+ (kill-buffer (current-buffer)))
+ (test-move-buffer-and-file-teardown)))
+
+(ert-deftest test-move-buffer-and-file-interactive-prompts-if-target-exists ()
+ "Should prompt user when called interactively and target exists."
+ (test-move-buffer-and-file-setup)
+ (unwind-protect
+ (let* ((source-dir (cj/create-test-subdirectory "source"))
+ (target-dir (cj/create-test-subdirectory "target"))
+ (source-file (expand-file-name "test.txt" source-dir))
+ (target-file (expand-file-name "test.txt" target-dir))
+ (prompted nil))
+ (with-temp-file target-file
+ (insert "existing"))
+ (with-temp-file source-file
+ (insert "new"))
+ (find-file source-file)
+ ;; Mock yes-or-no-p to capture that it was called
+ (cl-letf (((symbol-function 'yes-or-no-p)
+ (lambda (prompt)
+ (setq prompted t)
+ t))
+ ((symbol-function 'read-directory-name)
+ (lambda (&rest _) target-dir)))
+ (call-interactively #'cj/move-buffer-and-file)
+ (should prompted))
+ (kill-buffer (current-buffer)))
+ (test-move-buffer-and-file-teardown)))
+
+(ert-deftest test-move-buffer-and-file-interactive-no-prompt-if-target-missing ()
+ "Should not prompt when called interactively if target doesn't exist."
+ (test-move-buffer-and-file-setup)
+ (unwind-protect
+ (let* ((source-dir (cj/create-test-subdirectory "source"))
+ (target-dir (cj/create-test-subdirectory "target"))
+ (source-file (expand-file-name "test.txt" source-dir))
+ (prompted nil))
+ (with-temp-file source-file
+ (insert "new"))
+ (find-file source-file)
+ ;; Mock yes-or-no-p to capture if it was called
+ (cl-letf (((symbol-function 'yes-or-no-p)
+ (lambda (prompt)
+ (setq prompted t)
+ t))
+ ((symbol-function 'read-directory-name)
+ (lambda (&rest _) target-dir)))
+ (call-interactively #'cj/move-buffer-and-file)
+ (should-not prompted))
+ (kill-buffer (current-buffer)))
+ (test-move-buffer-and-file-teardown)))
+
+(ert-deftest test-move-buffer-and-file-source-deleted-during-operation-should-error ()
+ "Should error if source file is deleted during operation."
+ (test-move-buffer-and-file-setup)
+ (unwind-protect
+ (let* ((source-dir (cj/create-test-subdirectory "source"))
+ (target-dir (cj/create-test-subdirectory "target"))
+ (source-file (expand-file-name "test.txt" source-dir)))
+ (with-temp-file source-file
+ (insert "content"))
+ (find-file source-file)
+ (delete-file source-file)
+ (should-error (cj/--move-buffer-and-file target-dir))
+ (kill-buffer (current-buffer)))
+ (test-move-buffer-and-file-teardown)))
+
+;;; Error Cases - Edge Cases
+
+(ert-deftest test-move-buffer-and-file-symlink-source-should-handle ()
+ "Should handle symbolic link as source."
+ (test-move-buffer-and-file-setup)
+ (unwind-protect
+ (let* ((source-dir (cj/create-test-subdirectory "source"))
+ (target-dir (cj/create-test-subdirectory "target"))
+ (real-file (expand-file-name "real.txt" source-dir))
+ (symlink (expand-file-name "link.txt" source-dir))
+ (target-file (expand-file-name "link.txt" target-dir)))
+ (with-temp-file real-file
+ (insert "content"))
+ (make-symbolic-link real-file symlink)
+ (find-file symlink)
+ (cj/--move-buffer-and-file target-dir)
+ (should (file-exists-p target-file))
+ (kill-buffer (current-buffer)))
+ (test-move-buffer-and-file-teardown)))
+
+(ert-deftest test-move-buffer-and-file-read-only-buffer-should-still-work ()
+ "Should work even if buffer is read-only."
+ (test-move-buffer-and-file-setup)
+ (unwind-protect
+ (let* ((source-dir (cj/create-test-subdirectory "source"))
+ (target-dir (cj/create-test-subdirectory "target"))
+ (source-file (expand-file-name "test.txt" source-dir))
+ (target-file (expand-file-name "test.txt" target-dir)))
+ (with-temp-file source-file
+ (insert "content"))
+ (find-file source-file)
+ (read-only-mode 1)
+ (cj/--move-buffer-and-file target-dir)
+ (should (file-exists-p target-file))
+ (should-not (file-exists-p source-file))
+ (kill-buffer (current-buffer)))
+ (test-move-buffer-and-file-teardown)))
+
+(provide 'test-custom-file-buffer-move-buffer-and-file)
+;;; test-custom-file-buffer-move-buffer-and-file.el ends here
diff --git a/tests/test-custom-file-buffer-rename-buffer-and-file.el b/tests/test-custom-file-buffer-rename-buffer-and-file.el
new file mode 100644
index 00000000..ca8acff8
--- /dev/null
+++ b/tests/test-custom-file-buffer-rename-buffer-and-file.el
@@ -0,0 +1,939 @@
+;;; test-custom-file-buffer-rename-buffer-and-file.el --- Tests for cj/--rename-buffer-and-file -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; Tests for the cj/--rename-buffer-and-file function from custom-file-buffer.el
+;;
+;; This is the internal (non-interactive) implementation that renames both the
+;; current buffer and its visited file. The interactive wrapper
+;; cj/rename-buffer-and-file handles user prompting and delegates to this
+;; implementation.
+
+;;; Code:
+
+(require 'ert)
+(require 'testutil-general)
+
+;; Add modules directory to load path
+(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory))
+
+;; Stub dependencies before loading the module
+(defvar cj/custom-keymap (make-sparse-keymap)
+ "Stub keymap for testing.")
+
+;; Stub ps-print package
+(provide 'ps-print)
+
+;; Now load the actual production module
+(require 'custom-file-buffer)
+
+;;; Setup and Teardown
+
+(defun test-rename-buffer-and-file-setup ()
+ "Setup for rename-buffer-and-file tests."
+ (cj/create-test-base-dir))
+
+(defun test-rename-buffer-and-file-teardown ()
+ "Teardown for rename-buffer-and-file tests."
+ ;; Kill all buffers visiting files in test directory
+ (dolist (buf (buffer-list))
+ (when (buffer-file-name buf)
+ (when (string-prefix-p cj/test-base-dir (buffer-file-name buf))
+ (with-current-buffer buf
+ (set-buffer-modified-p nil))
+ (kill-buffer buf))))
+ (cj/delete-test-base-dir))
+
+;;; Normal Cases
+
+(ert-deftest test-rename-buffer-and-file-simple-rename ()
+ "Should rename file in same directory."
+ (test-rename-buffer-and-file-setup)
+ (unwind-protect
+ (let* ((test-dir (cj/create-test-subdirectory "test"))
+ (old-file (expand-file-name "old.txt" test-dir))
+ (new-file (expand-file-name "new.txt" test-dir)))
+ (with-temp-file old-file
+ (insert "content"))
+ (find-file old-file)
+ (cj/--rename-buffer-and-file "new.txt")
+ (should (file-exists-p new-file))
+ (should-not (file-exists-p old-file))
+ (should (string= (buffer-name) "new.txt"))
+ (kill-buffer (current-buffer)))
+ (test-rename-buffer-and-file-teardown)))
+
+(ert-deftest test-rename-buffer-and-file-different-directory ()
+ "Should rename to absolute path in different directory."
+ (test-rename-buffer-and-file-setup)
+ (unwind-protect
+ (let* ((source-dir (cj/create-test-subdirectory "source"))
+ (target-dir (cj/create-test-subdirectory "target"))
+ (old-file (expand-file-name "file.txt" source-dir))
+ (new-file (expand-file-name "renamed.txt" target-dir)))
+ (with-temp-file old-file
+ (insert "content"))
+ (find-file old-file)
+ (cj/--rename-buffer-and-file new-file)
+ (should (file-exists-p new-file))
+ (should-not (file-exists-p old-file))
+ (kill-buffer (current-buffer)))
+ (test-rename-buffer-and-file-teardown)))
+
+(ert-deftest test-rename-buffer-and-file-different-extension ()
+ "Should change file extension."
+ (test-rename-buffer-and-file-setup)
+ (unwind-protect
+ (let* ((test-dir (cj/create-test-subdirectory "test"))
+ (old-file (expand-file-name "file.txt" test-dir))
+ (new-file (expand-file-name "file.md" test-dir)))
+ (with-temp-file old-file
+ (insert "content"))
+ (find-file old-file)
+ (cj/--rename-buffer-and-file "file.md")
+ (should (file-exists-p new-file))
+ (should-not (file-exists-p old-file))
+ (should (string= (buffer-name) "file.md"))
+ (kill-buffer (current-buffer)))
+ (test-rename-buffer-and-file-teardown)))
+
+(ert-deftest test-rename-buffer-and-file-preserves-content ()
+ "Should preserve file content after rename."
+ (test-rename-buffer-and-file-setup)
+ (unwind-protect
+ (let* ((test-dir (cj/create-test-subdirectory "test"))
+ (old-file (expand-file-name "old.txt" test-dir))
+ (content "Important content\nWith multiple lines"))
+ (with-temp-file old-file
+ (insert content))
+ (find-file old-file)
+ (cj/--rename-buffer-and-file "new.txt")
+ (should (string= (buffer-string) content))
+ (kill-buffer (current-buffer)))
+ (test-rename-buffer-and-file-teardown)))
+
+(ert-deftest test-rename-buffer-and-file-updates-buffer-name ()
+ "Should update buffer name to match new filename."
+ (test-rename-buffer-and-file-setup)
+ (unwind-protect
+ (let* ((test-dir (cj/create-test-subdirectory "test"))
+ (old-file (expand-file-name "old.txt" test-dir)))
+ (with-temp-file old-file
+ (insert "content"))
+ (find-file old-file)
+ (should (string= (buffer-name) "old.txt"))
+ (cj/--rename-buffer-and-file "new.txt")
+ (should (string= (buffer-name) "new.txt"))
+ (kill-buffer (current-buffer)))
+ (test-rename-buffer-and-file-teardown)))
+
+(ert-deftest test-rename-buffer-and-file-updates-buffer-file-name ()
+ "Should update buffer-file-name correctly."
+ (test-rename-buffer-and-file-setup)
+ (unwind-protect
+ (let* ((test-dir (cj/create-test-subdirectory "test"))
+ (old-file (expand-file-name "old.txt" test-dir))
+ (new-file (expand-file-name "new.txt" test-dir)))
+ (with-temp-file old-file
+ (insert "content"))
+ (find-file old-file)
+ (cj/--rename-buffer-and-file "new.txt")
+ (should (string= (buffer-file-name) new-file))
+ (kill-buffer (current-buffer)))
+ (test-rename-buffer-and-file-teardown)))
+
+(ert-deftest test-rename-buffer-and-file-clears-modified-flag ()
+ "Should clear modified flag after rename."
+ (test-rename-buffer-and-file-setup)
+ (unwind-protect
+ (let* ((test-dir (cj/create-test-subdirectory "test"))
+ (old-file (expand-file-name "old.txt" test-dir)))
+ (with-temp-file old-file
+ (insert "content"))
+ (find-file old-file)
+ (insert "modification")
+ (should (buffer-modified-p))
+ (cj/--rename-buffer-and-file "new.txt")
+ (should-not (buffer-modified-p))
+ (kill-buffer (current-buffer)))
+ (test-rename-buffer-and-file-teardown)))
+
+(ert-deftest test-rename-buffer-and-file-returns-t-on-success ()
+ "Should return t when successful."
+ (test-rename-buffer-and-file-setup)
+ (unwind-protect
+ (let* ((test-dir (cj/create-test-subdirectory "test"))
+ (old-file (expand-file-name "old.txt" test-dir)))
+ (with-temp-file old-file
+ (insert "content"))
+ (find-file old-file)
+ (should (eq t (cj/--rename-buffer-and-file "new.txt")))
+ (kill-buffer (current-buffer)))
+ (test-rename-buffer-and-file-teardown)))
+
+;;; Boundary Cases - Naming
+
+(ert-deftest test-rename-buffer-and-file-unicode-in-name ()
+ "Should handle Unicode characters in name."
+ (test-rename-buffer-and-file-setup)
+ (unwind-protect
+ (let* ((test-dir (cj/create-test-subdirectory "test"))
+ (old-file (expand-file-name "old.txt" test-dir))
+ (new-file (expand-file-name "café.txt" test-dir)))
+ (with-temp-file old-file
+ (insert "content"))
+ (find-file old-file)
+ (cj/--rename-buffer-and-file "café.txt")
+ (should (file-exists-p new-file))
+ (kill-buffer (current-buffer)))
+ (test-rename-buffer-and-file-teardown)))
+
+(ert-deftest test-rename-buffer-and-file-emoji-in-name ()
+ "Should handle emoji characters in name."
+ (test-rename-buffer-and-file-setup)
+ (unwind-protect
+ (let* ((test-dir (cj/create-test-subdirectory "test"))
+ (old-file (expand-file-name "old.txt" test-dir))
+ (new-file (expand-file-name "test-🎉.txt" test-dir)))
+ (with-temp-file old-file
+ (insert "content"))
+ (find-file old-file)
+ (cj/--rename-buffer-and-file "test-🎉.txt")
+ (should (file-exists-p new-file))
+ (kill-buffer (current-buffer)))
+ (test-rename-buffer-and-file-teardown)))
+
+(ert-deftest test-rename-buffer-and-file-rtl-text-in-name ()
+ "Should handle RTL text in name."
+ (test-rename-buffer-and-file-setup)
+ (unwind-protect
+ (let* ((test-dir (cj/create-test-subdirectory "test"))
+ (old-file (expand-file-name "old.txt" test-dir))
+ (new-file (expand-file-name "مرحبا.txt" test-dir)))
+ (with-temp-file old-file
+ (insert "content"))
+ (find-file old-file)
+ (cj/--rename-buffer-and-file "مرحبا.txt")
+ (should (file-exists-p new-file))
+ (kill-buffer (current-buffer)))
+ (test-rename-buffer-and-file-teardown)))
+
+(ert-deftest test-rename-buffer-and-file-spaces-in-name ()
+ "Should handle spaces in name."
+ (test-rename-buffer-and-file-setup)
+ (unwind-protect
+ (let* ((test-dir (cj/create-test-subdirectory "test"))
+ (old-file (expand-file-name "old.txt" test-dir))
+ (new-file (expand-file-name "my new file.txt" test-dir)))
+ (with-temp-file old-file
+ (insert "content"))
+ (find-file old-file)
+ (cj/--rename-buffer-and-file "my new file.txt")
+ (should (file-exists-p new-file))
+ (kill-buffer (current-buffer)))
+ (test-rename-buffer-and-file-teardown)))
+
+(ert-deftest test-rename-buffer-and-file-special-chars-in-name ()
+ "Should handle special characters in name."
+ (test-rename-buffer-and-file-setup)
+ (unwind-protect
+ (let* ((test-dir (cj/create-test-subdirectory "test"))
+ (old-file (expand-file-name "old.txt" test-dir))
+ (new-file (expand-file-name "[test]-(1).txt" test-dir)))
+ (with-temp-file old-file
+ (insert "content"))
+ (find-file old-file)
+ (cj/--rename-buffer-and-file "[test]-(1).txt")
+ (should (file-exists-p new-file))
+ (kill-buffer (current-buffer)))
+ (test-rename-buffer-and-file-teardown)))
+
+(ert-deftest test-rename-buffer-and-file-very-long-name ()
+ "Should handle very long filename."
+ (test-rename-buffer-and-file-setup)
+ (unwind-protect
+ (let* ((test-dir (cj/create-test-subdirectory "test"))
+ (old-file (expand-file-name "old.txt" test-dir))
+ (long-name (concat (make-string 200 ?x) ".txt"))
+ (new-file (expand-file-name long-name test-dir)))
+ (with-temp-file old-file
+ (insert "content"))
+ (find-file old-file)
+ (cj/--rename-buffer-and-file long-name)
+ (should (file-exists-p new-file))
+ (kill-buffer (current-buffer)))
+ (test-rename-buffer-and-file-teardown)))
+
+(ert-deftest test-rename-buffer-and-file-single-char-name ()
+ "Should handle single character name."
+ (test-rename-buffer-and-file-setup)
+ (unwind-protect
+ (let* ((test-dir (cj/create-test-subdirectory "test"))
+ (old-file (expand-file-name "old.txt" test-dir))
+ (new-file (expand-file-name "x" test-dir)))
+ (with-temp-file old-file
+ (insert "content"))
+ (find-file old-file)
+ (cj/--rename-buffer-and-file "x")
+ (should (file-exists-p new-file))
+ (kill-buffer (current-buffer)))
+ (test-rename-buffer-and-file-teardown)))
+
+(ert-deftest test-rename-buffer-and-file-multiple-dots ()
+ "Should handle multiple dots in name."
+ (test-rename-buffer-and-file-setup)
+ (unwind-protect
+ (let* ((test-dir (cj/create-test-subdirectory "test"))
+ (old-file (expand-file-name "old.txt" test-dir))
+ (new-file (expand-file-name "my.file.name.txt" test-dir)))
+ (with-temp-file old-file
+ (insert "content"))
+ (find-file old-file)
+ (cj/--rename-buffer-and-file "my.file.name.txt")
+ (should (file-exists-p new-file))
+ (kill-buffer (current-buffer)))
+ (test-rename-buffer-and-file-teardown)))
+
+(ert-deftest test-rename-buffer-and-file-no-extension ()
+ "Should handle files without extension."
+ (test-rename-buffer-and-file-setup)
+ (unwind-protect
+ (let* ((test-dir (cj/create-test-subdirectory "test"))
+ (old-file (expand-file-name "old.txt" test-dir))
+ (new-file (expand-file-name "README" test-dir)))
+ (with-temp-file old-file
+ (insert "content"))
+ (find-file old-file)
+ (cj/--rename-buffer-and-file "README")
+ (should (file-exists-p new-file))
+ (kill-buffer (current-buffer)))
+ (test-rename-buffer-and-file-teardown)))
+
+(ert-deftest test-rename-buffer-and-file-hidden-file ()
+ "Should handle hidden files (starting with dot)."
+ (test-rename-buffer-and-file-setup)
+ (unwind-protect
+ (let* ((test-dir (cj/create-test-subdirectory "test"))
+ (old-file (expand-file-name "old.txt" test-dir))
+ (new-file (expand-file-name ".hidden" test-dir)))
+ (with-temp-file old-file
+ (insert "content"))
+ (find-file old-file)
+ (cj/--rename-buffer-and-file ".hidden")
+ (should (file-exists-p new-file))
+ (kill-buffer (current-buffer)))
+ (test-rename-buffer-and-file-teardown)))
+
+(ert-deftest test-rename-buffer-and-file-trailing-whitespace ()
+ "Should handle trailing/leading spaces in name."
+ (test-rename-buffer-and-file-setup)
+ (unwind-protect
+ (let* ((test-dir (cj/create-test-subdirectory "test"))
+ (old-file (expand-file-name "old.txt" test-dir))
+ (new-file (expand-file-name " spaced " test-dir)))
+ (with-temp-file old-file
+ (insert "content"))
+ (find-file old-file)
+ (cj/--rename-buffer-and-file " spaced ")
+ (should (file-exists-p new-file))
+ (kill-buffer (current-buffer)))
+ (test-rename-buffer-and-file-teardown)))
+
+(ert-deftest test-rename-buffer-and-file-only-changes-case ()
+ "Should handle case-only rename on case-sensitive filesystems."
+ (test-rename-buffer-and-file-setup)
+ (unwind-protect
+ (let* ((test-dir (cj/create-test-subdirectory "test"))
+ (old-file (expand-file-name "test.txt" test-dir))
+ (new-file (expand-file-name "TEST.txt" test-dir)))
+ (with-temp-file old-file
+ (insert "content"))
+ (find-file old-file)
+ ;; On case-insensitive systems, need ok-if-exists
+ (cj/--rename-buffer-and-file "TEST.txt" t)
+ (should (string= (buffer-name) "TEST.txt"))
+ (kill-buffer (current-buffer)))
+ (test-rename-buffer-and-file-teardown)))
+
+(ert-deftest test-rename-buffer-and-file-adds-extension ()
+ "Should handle adding extension to file."
+ (test-rename-buffer-and-file-setup)
+ (unwind-protect
+ (let* ((test-dir (cj/create-test-subdirectory "test"))
+ (old-file (expand-file-name "file" test-dir))
+ (new-file (expand-file-name "file.txt" test-dir)))
+ (with-temp-file old-file
+ (insert "content"))
+ (find-file old-file)
+ (cj/--rename-buffer-and-file "file.txt")
+ (should (file-exists-p new-file))
+ (kill-buffer (current-buffer)))
+ (test-rename-buffer-and-file-teardown)))
+
+(ert-deftest test-rename-buffer-and-file-removes-extension ()
+ "Should handle removing extension from file."
+ (test-rename-buffer-and-file-setup)
+ (unwind-protect
+ (let* ((test-dir (cj/create-test-subdirectory "test"))
+ (old-file (expand-file-name "file.txt" test-dir))
+ (new-file (expand-file-name "file" test-dir)))
+ (with-temp-file old-file
+ (insert "content"))
+ (find-file old-file)
+ (cj/--rename-buffer-and-file "file")
+ (should (file-exists-p new-file))
+ (kill-buffer (current-buffer)))
+ (test-rename-buffer-and-file-teardown)))
+
+(ert-deftest test-rename-buffer-and-file-just-extension ()
+ "Should handle name that is just extension."
+ (test-rename-buffer-and-file-setup)
+ (unwind-protect
+ (let* ((test-dir (cj/create-test-subdirectory "test"))
+ (old-file (expand-file-name "old.txt" test-dir))
+ (new-file (expand-file-name ".gitignore" test-dir)))
+ (with-temp-file old-file
+ (insert "content"))
+ (find-file old-file)
+ (cj/--rename-buffer-and-file ".gitignore")
+ (should (file-exists-p new-file))
+ (kill-buffer (current-buffer)))
+ (test-rename-buffer-and-file-teardown)))
+
+;;; Boundary Cases - Path Handling
+
+(ert-deftest test-rename-buffer-and-file-relative-path ()
+ "Should handle relative path."
+ (test-rename-buffer-and-file-setup)
+ (unwind-protect
+ (let* ((source-dir (cj/create-test-subdirectory "source"))
+ (target-dir (cj/create-test-subdirectory "target"))
+ (old-file (expand-file-name "file.txt" source-dir))
+ (new-file (expand-file-name "renamed.txt" target-dir)))
+ (with-temp-file old-file
+ (insert "content"))
+ (find-file old-file)
+ (cj/--rename-buffer-and-file "../target/renamed.txt")
+ (should (file-exists-p new-file))
+ (kill-buffer (current-buffer)))
+ (test-rename-buffer-and-file-teardown)))
+
+(ert-deftest test-rename-buffer-and-file-absolute-path ()
+ "Should handle absolute path."
+ (test-rename-buffer-and-file-setup)
+ (unwind-protect
+ (let* ((source-dir (cj/create-test-subdirectory "source"))
+ (target-dir (cj/create-test-subdirectory "target"))
+ (old-file (expand-file-name "file.txt" source-dir))
+ (new-file (expand-file-name "renamed.txt" target-dir)))
+ (with-temp-file old-file
+ (insert "content"))
+ (find-file old-file)
+ (cj/--rename-buffer-and-file new-file)
+ (should (file-exists-p new-file))
+ (kill-buffer (current-buffer)))
+ (test-rename-buffer-and-file-teardown)))
+
+(ert-deftest test-rename-buffer-and-file-parent-directory ()
+ "Should handle parent directory reference."
+ (test-rename-buffer-and-file-setup)
+ (unwind-protect
+ (let* ((parent-dir (cj/create-test-subdirectory "parent"))
+ (source-dir (cj/create-test-subdirectory "parent/source"))
+ (old-file (expand-file-name "file.txt" source-dir))
+ (new-file (expand-file-name "renamed.txt" parent-dir)))
+ (with-temp-file old-file
+ (insert "content"))
+ (find-file old-file)
+ (cj/--rename-buffer-and-file "../renamed.txt")
+ (should (file-exists-p new-file))
+ (kill-buffer (current-buffer)))
+ (test-rename-buffer-and-file-teardown)))
+
+(ert-deftest test-rename-buffer-and-file-deeply-nested-target ()
+ "Should handle deeply nested target directory."
+ (test-rename-buffer-and-file-setup)
+ (unwind-protect
+ (let* ((source-dir (cj/create-test-subdirectory "source"))
+ (target-dir (cj/create-test-subdirectory "a/b/c/d/target"))
+ (old-file (expand-file-name "file.txt" source-dir))
+ (new-file (expand-file-name "renamed.txt" target-dir)))
+ (with-temp-file old-file
+ (insert "content"))
+ (find-file old-file)
+ (cj/--rename-buffer-and-file new-file)
+ (should (file-exists-p new-file))
+ (kill-buffer (current-buffer)))
+ (test-rename-buffer-and-file-teardown)))
+
+(ert-deftest test-rename-buffer-and-file-same-directory-basename-only ()
+ "Should rename in same directory using just basename."
+ (test-rename-buffer-and-file-setup)
+ (unwind-protect
+ (let* ((test-dir (cj/create-test-subdirectory "test"))
+ (old-file (expand-file-name "old.txt" test-dir))
+ (new-file (expand-file-name "new.txt" test-dir)))
+ (with-temp-file old-file
+ (insert "content"))
+ (find-file old-file)
+ (cj/--rename-buffer-and-file "new.txt")
+ (should (file-exists-p new-file))
+ (should-not (file-exists-p old-file))
+ (kill-buffer (current-buffer)))
+ (test-rename-buffer-and-file-teardown)))
+
+(ert-deftest test-rename-buffer-and-file-expand-tilde ()
+ "Should expand tilde in path."
+ (test-rename-buffer-and-file-setup)
+ (unwind-protect
+ (let* ((test-dir (cj/create-test-subdirectory "test"))
+ (old-file (expand-file-name "old.txt" test-dir))
+ ;; Use a path relative to home that we can create
+ (home-test-dir (expand-file-name "temp-test-rename" "~"))
+ (new-file (expand-file-name "renamed.txt" home-test-dir)))
+ (make-directory home-test-dir t)
+ (with-temp-file old-file
+ (insert "content"))
+ (find-file old-file)
+ (cj/--rename-buffer-and-file (concat "~/temp-test-rename/renamed.txt"))
+ (should (file-exists-p new-file))
+ (kill-buffer (current-buffer))
+ (delete-directory home-test-dir t))
+ (test-rename-buffer-and-file-teardown)))
+
+;;; Boundary Cases - File Content
+
+(ert-deftest test-rename-buffer-and-file-empty-file ()
+ "Should handle empty file."
+ (test-rename-buffer-and-file-setup)
+ (unwind-protect
+ (let* ((test-dir (cj/create-test-subdirectory "test"))
+ (old-file (expand-file-name "old.txt" test-dir))
+ (new-file (expand-file-name "new.txt" test-dir)))
+ (with-temp-file old-file)
+ (find-file old-file)
+ (cj/--rename-buffer-and-file "new.txt")
+ (should (file-exists-p new-file))
+ (should (= 0 (buffer-size)))
+ (kill-buffer (current-buffer)))
+ (test-rename-buffer-and-file-teardown)))
+
+(ert-deftest test-rename-buffer-and-file-large-file ()
+ "Should handle large file."
+ (test-rename-buffer-and-file-setup)
+ (unwind-protect
+ (let* ((test-dir (cj/create-test-subdirectory "test"))
+ (old-file (expand-file-name "old.txt" test-dir))
+ (large-content (make-string 100000 ?x)))
+ (with-temp-file old-file
+ (insert large-content))
+ (find-file old-file)
+ (cj/--rename-buffer-and-file "new.txt")
+ (should (string= (buffer-string) large-content))
+ (kill-buffer (current-buffer)))
+ (test-rename-buffer-and-file-teardown)))
+
+(ert-deftest test-rename-buffer-and-file-binary-content ()
+ "Should handle binary content."
+ (test-rename-buffer-and-file-setup)
+ (unwind-protect
+ (let* ((test-dir (cj/create-test-subdirectory "test"))
+ (old-file (expand-file-name "old.dat" test-dir))
+ (new-file (expand-file-name "new.dat" test-dir))
+ (binary-content (string 0 1 2 3 255 254 253)))
+ (with-temp-file old-file
+ (set-buffer-multibyte nil)
+ (insert binary-content))
+ (find-file old-file)
+ (cj/--rename-buffer-and-file "new.dat")
+ (should (file-exists-p new-file))
+ (kill-buffer (current-buffer)))
+ (test-rename-buffer-and-file-teardown)))
+
+(ert-deftest test-rename-buffer-and-file-preserves-newlines ()
+ "Should preserve different newline types."
+ (test-rename-buffer-and-file-setup)
+ (unwind-protect
+ (let* ((test-dir (cj/create-test-subdirectory "test"))
+ (old-file (expand-file-name "old.txt" test-dir))
+ (content "Line 1\nLine 2\n\nLine 4\n"))
+ (with-temp-file old-file
+ (insert content))
+ (find-file old-file)
+ (cj/--rename-buffer-and-file "new.txt")
+ (should (string= (buffer-string) content))
+ (kill-buffer (current-buffer)))
+ (test-rename-buffer-and-file-teardown)))
+
+(ert-deftest test-rename-buffer-and-file-preserves-encoding ()
+ "Should preserve UTF-8 encoded content."
+ (test-rename-buffer-and-file-setup)
+ (unwind-protect
+ (let* ((test-dir (cj/create-test-subdirectory "test"))
+ (old-file (expand-file-name "old.txt" test-dir))
+ (content "Hello 世界 مرحبا Привет"))
+ (with-temp-file old-file
+ (insert content))
+ (find-file old-file)
+ (cj/--rename-buffer-and-file "new.txt")
+ (should (string= (buffer-string) content))
+ (kill-buffer (current-buffer)))
+ (test-rename-buffer-and-file-teardown)))
+
+;;; Boundary Cases - Buffer State
+
+(ert-deftest test-rename-buffer-and-file-with-unsaved-changes ()
+ "Should handle buffer with unsaved changes."
+ (test-rename-buffer-and-file-setup)
+ (unwind-protect
+ (let* ((test-dir (cj/create-test-subdirectory "test"))
+ (old-file (expand-file-name "old.txt" test-dir)))
+ (with-temp-file old-file
+ (insert "original"))
+ (find-file old-file)
+ (insert " modified")
+ (should (buffer-modified-p))
+ (cj/--rename-buffer-and-file "new.txt")
+ (should-not (buffer-modified-p))
+ (kill-buffer (current-buffer)))
+ (test-rename-buffer-and-file-teardown)))
+
+(ert-deftest test-rename-buffer-and-file-multiple-windows ()
+ "Should work when buffer displayed in multiple windows."
+ (test-rename-buffer-and-file-setup)
+ (unwind-protect
+ (let* ((test-dir (cj/create-test-subdirectory "test"))
+ (old-file (expand-file-name "old.txt" test-dir)))
+ (with-temp-file old-file
+ (insert "content"))
+ (find-file old-file)
+ (delete-other-windows)
+ (split-window)
+ (other-window 1)
+ (switch-to-buffer (get-file-buffer old-file))
+ (cj/--rename-buffer-and-file "new.txt")
+ (should (string= (buffer-name) "new.txt"))
+ (kill-buffer (current-buffer))
+ (delete-other-windows))
+ (test-rename-buffer-and-file-teardown)))
+
+(ert-deftest test-rename-buffer-and-file-preserves-point ()
+ "Should preserve point position."
+ (test-rename-buffer-and-file-setup)
+ (unwind-protect
+ (let* ((test-dir (cj/create-test-subdirectory "test"))
+ (old-file (expand-file-name "old.txt" test-dir))
+ (content "Line 1\nLine 2\nLine 3\n"))
+ (with-temp-file old-file
+ (insert content))
+ (find-file old-file)
+ (goto-char (point-min))
+ (forward-line 1)
+ (let ((original-point (point)))
+ (cj/--rename-buffer-and-file "new.txt")
+ (should (= (point) original-point)))
+ (kill-buffer (current-buffer)))
+ (test-rename-buffer-and-file-teardown)))
+
+(ert-deftest test-rename-buffer-and-file-preserves-mark ()
+ "Should preserve mark."
+ (test-rename-buffer-and-file-setup)
+ (unwind-protect
+ (let* ((test-dir (cj/create-test-subdirectory "test"))
+ (old-file (expand-file-name "old.txt" test-dir))
+ (content "Line 1\nLine 2\nLine 3\n"))
+ (with-temp-file old-file
+ (insert content))
+ (find-file old-file)
+ (goto-char (point-min))
+ (set-mark (point))
+ (forward-line 2)
+ (let ((original-mark (mark)))
+ (cj/--rename-buffer-and-file "new.txt")
+ (should (= (mark) original-mark)))
+ (kill-buffer (current-buffer)))
+ (test-rename-buffer-and-file-teardown)))
+
+(ert-deftest test-rename-buffer-and-file-read-only-buffer ()
+ "Should work even with read-only buffer."
+ (test-rename-buffer-and-file-setup)
+ (unwind-protect
+ (let* ((test-dir (cj/create-test-subdirectory "test"))
+ (old-file (expand-file-name "old.txt" test-dir))
+ (new-file (expand-file-name "new.txt" test-dir)))
+ (with-temp-file old-file
+ (insert "content"))
+ (find-file old-file)
+ (read-only-mode 1)
+ (cj/--rename-buffer-and-file "new.txt")
+ (should (file-exists-p new-file))
+ (kill-buffer (current-buffer)))
+ (test-rename-buffer-and-file-teardown)))
+
+;;; Error Cases - Buffer Issues
+
+(ert-deftest test-rename-buffer-and-file-non-file-buffer-returns-nil ()
+ "Should return nil when buffer not visiting file."
+ (test-rename-buffer-and-file-setup)
+ (unwind-protect
+ (with-temp-buffer
+ (rename-buffer "non-file-buffer" t)
+ (let ((result (cj/--rename-buffer-and-file "new.txt")))
+ (should-not result)))
+ (test-rename-buffer-and-file-teardown)))
+
+(ert-deftest test-rename-buffer-and-file-scratch-buffer-returns-nil ()
+ "Should return nil for scratch buffer."
+ (test-rename-buffer-and-file-setup)
+ (unwind-protect
+ (with-current-buffer "*scratch*"
+ (let ((result (cj/--rename-buffer-and-file "new.txt")))
+ (should-not result)))
+ (test-rename-buffer-and-file-teardown)))
+
+(ert-deftest test-rename-buffer-and-file-buffer-name-exists-should-error ()
+ "Should error when buffer with new name exists."
+ (test-rename-buffer-and-file-setup)
+ (unwind-protect
+ (let* ((test-dir (cj/create-test-subdirectory "test"))
+ (file1 (expand-file-name "file1.txt" test-dir))
+ (file2 (expand-file-name "file2.txt" test-dir)))
+ (with-temp-file file1
+ (insert "content1"))
+ (with-temp-file file2
+ (insert "content2"))
+ (find-file file1)
+ (let ((buf1 (current-buffer)))
+ (find-file file2)
+ ;; Try to rename file2 to file1.txt (buffer exists)
+ (should-error (cj/--rename-buffer-and-file "file1.txt"))
+ (kill-buffer (current-buffer))
+ (kill-buffer buf1)))
+ (test-rename-buffer-and-file-teardown)))
+
+(ert-deftest test-rename-buffer-and-file-killed-buffer-should-error ()
+ "Should error when operating on killed buffer."
+ (test-rename-buffer-and-file-setup)
+ (unwind-protect
+ (let* ((test-dir (cj/create-test-subdirectory "test"))
+ (old-file (expand-file-name "old.txt" test-dir))
+ (buf nil))
+ (with-temp-file old-file
+ (insert "content"))
+ (setq buf (find-file old-file))
+ (kill-buffer buf)
+ (should-error
+ (with-current-buffer buf
+ (cj/--rename-buffer-and-file "new.txt"))))
+ (test-rename-buffer-and-file-teardown)))
+
+;;; Error Cases - File Conflicts
+
+(ert-deftest test-rename-buffer-and-file-target-exists-should-error-if-not-ok ()
+ "Should error when target exists and ok-if-exists is nil."
+ (test-rename-buffer-and-file-setup)
+ (unwind-protect
+ (let* ((test-dir (cj/create-test-subdirectory "test"))
+ (old-file (expand-file-name "old.txt" test-dir))
+ (new-file (expand-file-name "new.txt" test-dir)))
+ (with-temp-file old-file
+ (insert "old content"))
+ (with-temp-file new-file
+ (insert "existing content"))
+ (find-file old-file)
+ (should-error (cj/--rename-buffer-and-file "new.txt" nil))
+ ;; Old file should still exist since rename failed
+ (should (file-exists-p old-file))
+ (kill-buffer (current-buffer)))
+ (test-rename-buffer-and-file-teardown)))
+
+(ert-deftest test-rename-buffer-and-file-target-exists-should-overwrite-if-ok ()
+ "Should overwrite when target exists and ok-if-exists is t."
+ (test-rename-buffer-and-file-setup)
+ (unwind-protect
+ (let* ((test-dir (cj/create-test-subdirectory "test"))
+ (old-file (expand-file-name "old.txt" test-dir))
+ (new-file (expand-file-name "new.txt" test-dir))
+ (old-content "old content")
+ (new-content "existing content"))
+ (with-temp-file old-file
+ (insert old-content))
+ (with-temp-file new-file
+ (insert new-content))
+ (find-file old-file)
+ (cj/--rename-buffer-and-file "new.txt" t)
+ (should (file-exists-p new-file))
+ (should-not (file-exists-p old-file))
+ ;; Content should be from old file
+ (revert-buffer t t)
+ (should (string= (buffer-string) old-content))
+ (kill-buffer (current-buffer)))
+ (test-rename-buffer-and-file-teardown)))
+
+(ert-deftest test-rename-buffer-and-file-source-deleted-should-error ()
+ "Should error if source file deleted during operation."
+ (test-rename-buffer-and-file-setup)
+ (unwind-protect
+ (let* ((test-dir (cj/create-test-subdirectory "test"))
+ (old-file (expand-file-name "old.txt" test-dir)))
+ (with-temp-file old-file
+ (insert "content"))
+ (find-file old-file)
+ (delete-file old-file)
+ (should-error (cj/--rename-buffer-and-file "new.txt"))
+ (kill-buffer (current-buffer)))
+ (test-rename-buffer-and-file-teardown)))
+
+(ert-deftest test-rename-buffer-and-file-same-name-is-noop ()
+ "Should handle rename to same name as no-op."
+ (test-rename-buffer-and-file-setup)
+ (unwind-protect
+ (let* ((test-dir (cj/create-test-subdirectory "test"))
+ (old-file (expand-file-name "file.txt" test-dir)))
+ (with-temp-file old-file
+ (insert "content"))
+ (find-file old-file)
+ ;; Rename to same name with ok-if-exists
+ (cj/--rename-buffer-and-file "file.txt" t)
+ (should (file-exists-p old-file))
+ (should (string= (buffer-name) "file.txt"))
+ (kill-buffer (current-buffer)))
+ (test-rename-buffer-and-file-teardown)))
+
+;;; Error Cases - Path Issues
+
+(ert-deftest test-rename-buffer-and-file-nil-name-should-error ()
+ "Should error when new-name is nil."
+ (test-rename-buffer-and-file-setup)
+ (unwind-protect
+ (let* ((test-dir (cj/create-test-subdirectory "test"))
+ (old-file (expand-file-name "old.txt" test-dir)))
+ (with-temp-file old-file
+ (insert "content"))
+ (find-file old-file)
+ (should-error (cj/--rename-buffer-and-file nil))
+ (kill-buffer (current-buffer)))
+ (test-rename-buffer-and-file-teardown)))
+
+(ert-deftest test-rename-buffer-and-file-empty-name-should-error ()
+ "Should error when new-name is empty string."
+ (test-rename-buffer-and-file-setup)
+ (unwind-protect
+ (let* ((test-dir (cj/create-test-subdirectory "test"))
+ (old-file (expand-file-name "old.txt" test-dir)))
+ (with-temp-file old-file
+ (insert "content"))
+ (find-file old-file)
+ (should-error (cj/--rename-buffer-and-file ""))
+ (kill-buffer (current-buffer)))
+ (test-rename-buffer-and-file-teardown)))
+
+(ert-deftest test-rename-buffer-and-file-nonexistent-target-dir-should-error ()
+ "Should error when target directory doesn't exist."
+ (test-rename-buffer-and-file-setup)
+ (unwind-protect
+ (let* ((test-dir (cj/create-test-subdirectory "test"))
+ (old-file (expand-file-name "old.txt" test-dir))
+ (nonexistent-path (expand-file-name "nonexistent/new.txt" cj/test-base-dir)))
+ (with-temp-file old-file
+ (insert "content"))
+ (find-file old-file)
+ (should-error (cj/--rename-buffer-and-file nonexistent-path))
+ (kill-buffer (current-buffer)))
+ (test-rename-buffer-and-file-teardown)))
+
+(ert-deftest test-rename-buffer-and-file-target-is-directory-should-error ()
+ "Should error when new-name is existing directory."
+ (test-rename-buffer-and-file-setup)
+ (unwind-protect
+ (let* ((test-dir (cj/create-test-subdirectory "test"))
+ (target-dir (cj/create-test-subdirectory "target"))
+ (old-file (expand-file-name "old.txt" test-dir)))
+ (with-temp-file old-file
+ (insert "content"))
+ (find-file old-file)
+ (should-error (cj/--rename-buffer-and-file target-dir))
+ (kill-buffer (current-buffer)))
+ (test-rename-buffer-and-file-teardown)))
+
+;;; Error Cases - Permissions
+
+(ert-deftest test-rename-buffer-and-file-no-write-permission-target ()
+ "Should error when target directory not writable."
+ (test-rename-buffer-and-file-setup)
+ (unwind-protect
+ (let* ((source-dir (cj/create-test-subdirectory "source"))
+ (target-dir (cj/create-test-subdirectory "target"))
+ (old-file (expand-file-name "old.txt" source-dir))
+ (new-file (expand-file-name "new.txt" target-dir)))
+ (with-temp-file old-file
+ (insert "content"))
+ (set-file-modes target-dir #o555)
+ (find-file old-file)
+ (should-error (cj/--rename-buffer-and-file new-file))
+ (set-file-modes target-dir #o755)
+ (kill-buffer (current-buffer)))
+ (test-rename-buffer-and-file-teardown)))
+
+(ert-deftest test-rename-buffer-and-file-no-delete-permission-source-dir ()
+ "Should error when source directory doesn't allow deletion."
+ (test-rename-buffer-and-file-setup)
+ (unwind-protect
+ (let* ((source-dir (cj/create-test-subdirectory "source"))
+ (target-dir (cj/create-test-subdirectory "target"))
+ (old-file (expand-file-name "old.txt" source-dir))
+ (new-file (expand-file-name "new.txt" target-dir)))
+ (with-temp-file old-file
+ (insert "content"))
+ (find-file old-file)
+ (set-file-modes source-dir #o555)
+ (should-error (cj/--rename-buffer-and-file new-file))
+ (set-file-modes source-dir #o755)
+ (kill-buffer (current-buffer)))
+ (test-rename-buffer-and-file-teardown)))
+
+;;; Error Cases - Edge Cases
+
+(ert-deftest test-rename-buffer-and-file-symlink-source ()
+ "Should handle symbolic link as source."
+ (test-rename-buffer-and-file-setup)
+ (unwind-protect
+ (let* ((test-dir (cj/create-test-subdirectory "test"))
+ (real-file (expand-file-name "real.txt" test-dir))
+ (symlink (expand-file-name "link.txt" test-dir))
+ (new-file (expand-file-name "renamed.txt" test-dir)))
+ (with-temp-file real-file
+ (insert "content"))
+ (make-symbolic-link real-file symlink)
+ (find-file symlink)
+ (cj/--rename-buffer-and-file "renamed.txt")
+ (should (file-exists-p new-file))
+ (kill-buffer (current-buffer)))
+ (test-rename-buffer-and-file-teardown)))
+
+(ert-deftest test-rename-buffer-and-file-interactive-prompts-on-conflict ()
+ "Should prompt user when called interactively and file exists."
+ (test-rename-buffer-and-file-setup)
+ (unwind-protect
+ (let* ((test-dir (cj/create-test-subdirectory "test"))
+ (old-file (expand-file-name "old.txt" test-dir))
+ (new-file (expand-file-name "new.txt" test-dir))
+ (prompted nil))
+ (with-temp-file old-file
+ (insert "old"))
+ (with-temp-file new-file
+ (insert "existing"))
+ (find-file old-file)
+ ;; Mock yes-or-no-p to capture that it was called
+ (cl-letf (((symbol-function 'yes-or-no-p)
+ (lambda (prompt)
+ (setq prompted t)
+ t))
+ ((symbol-function 'read-string)
+ (lambda (&rest _) "new.txt")))
+ (call-interactively #'cj/rename-buffer-and-file)
+ (should prompted))
+ (kill-buffer (current-buffer)))
+ (test-rename-buffer-and-file-teardown)))
+
+(provide 'test-custom-file-buffer-rename-buffer-and-file)
+;;; test-custom-file-buffer-rename-buffer-and-file.el ends here
diff --git a/tests/test-custom-misc-count-words.el b/tests/test-custom-misc-count-words.el
new file mode 100644
index 00000000..f2bf793f
--- /dev/null
+++ b/tests/test-custom-misc-count-words.el
@@ -0,0 +1,148 @@
+;;; test-custom-misc-count-words.el --- Tests for cj/--count-words -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; Tests for the cj/--count-words function from custom-misc.el
+;;
+;; This function counts words in a region using Emacs's built-in count-words.
+;; A word is defined by Emacs's word boundaries, which generally means
+;; sequences of word-constituent characters separated by whitespace or punctuation.
+;;
+;; We test the NON-INTERACTIVE implementation (cj/--count-words) to avoid
+;; mocking region selection. This follows our testing best practice of
+;; separating business logic from UI interaction.
+
+;;; Code:
+
+(require 'ert)
+(require 'testutil-general)
+
+;; Add modules directory to load path
+(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory))
+
+;; Stub dependencies before loading the module
+(defvar cj/custom-keymap (make-sparse-keymap)
+ "Stub keymap for testing.")
+
+;; Now load the actual production module
+(require 'custom-misc)
+
+;;; Test Helpers
+
+(defun test-count-words (input-text)
+ "Test cj/--count-words on INPUT-TEXT.
+Returns the word count."
+ (with-temp-buffer
+ (insert input-text)
+ (cj/--count-words (point-min) (point-max))))
+
+;;; Normal Cases
+
+(ert-deftest test-count-words-multiple-words ()
+ "Should count multiple words."
+ (should (= 5 (test-count-words "The quick brown fox jumps"))))
+
+(ert-deftest test-count-words-single-word ()
+ "Should count single word."
+ (should (= 1 (test-count-words "hello"))))
+
+(ert-deftest test-count-words-with-punctuation ()
+ "Should count words with punctuation."
+ (should (= 5 (test-count-words "Hello, world! How are you?"))))
+
+(ert-deftest test-count-words-multiple-spaces ()
+ "Should count words separated by multiple spaces."
+ (should (= 3 (test-count-words "hello world test"))))
+
+(ert-deftest test-count-words-with-newlines ()
+ "Should count words across newlines."
+ (should (= 6 (test-count-words "line one\nline two\nline three"))))
+
+(ert-deftest test-count-words-with-tabs ()
+ "Should count words separated by tabs."
+ (should (= 3 (test-count-words "hello\tworld\ttest"))))
+
+(ert-deftest test-count-words-mixed-whitespace ()
+ "Should count words with mixed whitespace."
+ (should (= 4 (test-count-words "hello \t world\n\ntest end"))))
+
+(ert-deftest test-count-words-hyphenated ()
+ "Should count hyphenated words."
+ ;; Emacs treats hyphens as word separators in count-words
+ (should (= 7 (test-count-words "This is state-of-the-art technology"))))
+
+(ert-deftest test-count-words-contractions ()
+ "Should count contractions."
+ ;; Emacs treats apostrophes as word separators in count-words
+ (should (= 6 (test-count-words "don't can't won't"))))
+
+(ert-deftest test-count-words-numbers ()
+ "Should count numbers as words."
+ (should (= 6 (test-count-words "The year 2024 has 365 days"))))
+
+;;; Boundary Cases
+
+(ert-deftest test-count-words-empty-string ()
+ "Should return 0 for empty string."
+ (should (= 0 (test-count-words ""))))
+
+(ert-deftest test-count-words-only-whitespace ()
+ "Should return 0 for whitespace-only text."
+ (should (= 0 (test-count-words " \t\n\n "))))
+
+(ert-deftest test-count-words-only-punctuation ()
+ "Should count punctuation-only text."
+ ;; Emacs may count consecutive punctuation as a word
+ (should (= 1 (test-count-words "!@#$%^&*()"))))
+
+(ert-deftest test-count-words-leading-trailing-spaces ()
+ "Should count words ignoring leading/trailing spaces."
+ (should (= 3 (test-count-words " hello world test "))))
+
+(ert-deftest test-count-words-unicode ()
+ "Should count Unicode words."
+ (should (= 3 (test-count-words "café résumé naïve"))))
+
+(ert-deftest test-count-words-very-long-text ()
+ "Should handle very long text."
+ (let ((long-text (mapconcat (lambda (_) "word") (make-list 1000 nil) " ")))
+ (should (= 1000 (test-count-words long-text)))))
+
+(ert-deftest test-count-words-multiline-paragraph ()
+ "Should count words in multi-line paragraph."
+ (let ((text "This is a paragraph
+that spans multiple
+lines with various
+words in it."))
+ (should (= 13 (test-count-words text)))))
+
+;;; Error Cases
+
+(ert-deftest test-count-words-start-greater-than-end ()
+ "Should error when start > end."
+ (should-error
+ (with-temp-buffer
+ (insert "hello world")
+ (cj/--count-words (point-max) (point-min)))
+ :type 'error))
+
+(ert-deftest test-count-words-empty-region ()
+ "Should return 0 for empty region (start == end)."
+ (with-temp-buffer
+ (insert "hello world")
+ (let ((pos (/ (+ (point-min) (point-max)) 2)))
+ (should (= 0 (cj/--count-words pos pos))))))
+
+(ert-deftest test-count-words-partial-region ()
+ "Should count words only in specified region."
+ (with-temp-buffer
+ (insert "one two three four five")
+ ;; Count only "two three four" (positions roughly in middle)
+ (goto-char (point-min))
+ (search-forward "two")
+ (let ((start (match-beginning 0)))
+ (search-forward "four")
+ (let ((end (match-end 0)))
+ (should (= 3 (cj/--count-words start end)))))))
+
+(provide 'test-custom-misc-count-words)
+;;; test-custom-misc-count-words.el ends here
diff --git a/tests/test-custom-misc-format-region.el b/tests/test-custom-misc-format-region.el
new file mode 100644
index 00000000..c40a8898
--- /dev/null
+++ b/tests/test-custom-misc-format-region.el
@@ -0,0 +1,161 @@
+;;; test-custom-misc-format-region.el --- Tests for cj/--format-region -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; Tests for the cj/--format-region function from custom-misc.el
+;;
+;; This function reformats text by applying three operations:
+;; 1. untabify - converts tabs to spaces
+;; 2. indent-region - reindents according to major mode
+;; 3. delete-trailing-whitespace - removes trailing whitespace
+;;
+;; Note: indent-region behavior is major-mode dependent. We test in
+;; emacs-lisp-mode and fundamental-mode for predictable results.
+;;
+;; We test the NON-INTERACTIVE implementation (cj/--format-region)
+;; to avoid mocking region selection. This follows our testing best practice
+;; of separating business logic from UI interaction.
+
+;;; Code:
+
+(require 'ert)
+(require 'testutil-general)
+
+;; Add modules directory to load path
+(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory))
+
+;; Stub dependencies before loading the module
+(defvar cj/custom-keymap (make-sparse-keymap)
+ "Stub keymap for testing.")
+
+;; Now load the actual production module
+(require 'custom-misc)
+
+;;; Test Helpers
+
+(defun test-format-region (input-text &optional mode)
+ "Test cj/--format-region on INPUT-TEXT.
+MODE is the major mode to use (defaults to fundamental-mode).
+Returns the buffer string after operation."
+ (with-temp-buffer
+ (funcall (or mode #'fundamental-mode))
+ (insert input-text)
+ (cj/--format-region (point-min) (point-max))
+ (buffer-string)))
+
+;;; Normal Cases - Tab Conversion
+
+(ert-deftest test-format-region-converts-tabs ()
+ "Should convert tabs to spaces."
+ (let ((result (test-format-region "hello\tworld")))
+ (should-not (string-match-p "\t" result))
+ (should (string-match-p " " result))))
+
+(ert-deftest test-format-region-multiple-tabs ()
+ "Should convert multiple tabs."
+ (let ((result (test-format-region "\t\thello\t\tworld\t\t")))
+ (should-not (string-match-p "\t" result))))
+
+;;; Normal Cases - Trailing Whitespace
+
+(ert-deftest test-format-region-removes-trailing-spaces ()
+ "Should remove trailing spaces."
+ (let ((result (test-format-region "hello world ")))
+ (should (string= result "hello world"))))
+
+(ert-deftest test-format-region-removes-trailing-tabs ()
+ "Should remove trailing tabs."
+ (let ((result (test-format-region "hello world\t\t")))
+ (should (string= result "hello world"))))
+
+(ert-deftest test-format-region-removes-trailing-mixed ()
+ "Should remove trailing mixed whitespace."
+ (let ((result (test-format-region "hello world \t \t ")))
+ (should (string= result "hello world"))))
+
+(ert-deftest test-format-region-multiline-trailing ()
+ "Should remove trailing whitespace from multiple lines."
+ (let ((result (test-format-region "line1 \nline2\t\t\nline3 \t ")))
+ (should (string= result "line1\nline2\nline3"))))
+
+;;; Normal Cases - Combined Operations
+
+(ert-deftest test-format-region-tabs-and-trailing ()
+ "Should handle both tabs and trailing whitespace."
+ (let ((result (test-format-region "\thello\tworld\t\t")))
+ (should-not (string-match-p "\t" result))
+ ;; Should not end with whitespace
+ (should-not (string-match-p "[ \t]+$" result))))
+
+(ert-deftest test-format-region-preserves-interior-spaces ()
+ "Should preserve interior spaces while fixing edges."
+ (let ((result (test-format-region "\thello world\t")))
+ (should (string-match-p "hello world" result))
+ (should-not (string-match-p "\t" result))))
+
+;;; Normal Cases - Indentation (Mode-Specific)
+
+(ert-deftest test-format-region-elisp-indentation ()
+ "Should reindent Elisp code."
+ (let* ((input "(defun foo ()\n(+ 1 2))")
+ (result (test-format-region input #'emacs-lisp-mode))
+ (lines (split-string result "\n")))
+ ;; The inner form should be indented - second line should start with 2 spaces
+ (should (= 2 (length lines)))
+ (should (string-prefix-p "(defun foo ()" (car lines)))
+ (should (string-prefix-p " " (cadr lines)))))
+
+;;; Boundary Cases
+
+(ert-deftest test-format-region-empty-string ()
+ "Should handle empty string."
+ (let ((result (test-format-region "")))
+ (should (string= result ""))))
+
+(ert-deftest test-format-region-no-issues ()
+ "Should handle text with no formatting issues (no-op)."
+ (let ((result (test-format-region "hello world")))
+ (should (string= result "hello world"))))
+
+(ert-deftest test-format-region-only-whitespace ()
+ "Should handle text with only whitespace."
+ (let ((result (test-format-region "\t \t ")))
+ ;; Should become empty or just spaces, no tabs
+ (should-not (string-match-p "\t" result))))
+
+(ert-deftest test-format-region-single-line ()
+ "Should handle single line."
+ (let ((result (test-format-region "\thello\t")))
+ (should-not (string-match-p "\t" result))))
+
+(ert-deftest test-format-region-very-long-text ()
+ "Should handle very long text."
+ (let* ((long-text (mapconcat (lambda (_) "\thello\t") (make-list 100 nil) "\n"))
+ (result (test-format-region long-text)))
+ (should-not (string-match-p "\t" result))))
+
+(ert-deftest test-format-region-newlines-preserved ()
+ "Should preserve newlines while fixing formatting."
+ (let ((result (test-format-region "line1\t \nline2\t \nline3\t ")))
+ (should (= 2 (cl-count ?\n result)))))
+
+;;; Error Cases
+
+(ert-deftest test-format-region-start-greater-than-end ()
+ "Should error when start > end."
+ (should-error
+ (with-temp-buffer
+ (insert "hello world")
+ (cj/--format-region (point-max) (point-min)))
+ :type 'error))
+
+(ert-deftest test-format-region-empty-region ()
+ "Should handle empty region (start == end) without error."
+ (with-temp-buffer
+ (insert "hello world")
+ (let ((pos (/ (+ (point-min) (point-max)) 2)))
+ (cj/--format-region pos pos)
+ ;; Should complete without error
+ (should (string= (buffer-string) "hello world")))))
+
+(provide 'test-custom-misc-format-region)
+;;; test-custom-misc-format-region.el ends here
diff --git a/tests/test-custom-misc-jump-to-matching-paren.el b/tests/test-custom-misc-jump-to-matching-paren.el
new file mode 100644
index 00000000..973b6dfa
--- /dev/null
+++ b/tests/test-custom-misc-jump-to-matching-paren.el
@@ -0,0 +1,197 @@
+;;; test-custom-misc-jump-to-matching-paren.el --- Tests for cj/jump-to-matching-paren -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; Tests for the cj/jump-to-matching-paren function from custom-misc.el
+;;
+;; This function jumps to matching delimiters using Emacs's sexp navigation.
+;; It works with any delimiter that has matching syntax according to the
+;; current syntax table (parentheses, brackets, braces, etc.).
+;;
+;; Unlike other functions in this test suite, this is an INTERACTIVE function
+;; that moves point and displays messages. We test it as an integration test
+;; by setting up buffers, positioning point, calling the function, and
+;; verifying where point ends up.
+;;
+;; Key behaviors:
+;; - When on opening delimiter: jump forward to matching closing delimiter
+;; - When on closing delimiter: jump backward to matching opening delimiter
+;; - When just after closing delimiter: jump backward to matching opening
+;; - When not on delimiter: display message, don't move
+;; - When no matching delimiter: display error message, don't move
+
+;;; Code:
+
+(require 'ert)
+(require 'testutil-general)
+
+;; Add modules directory to load path
+(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory))
+
+;; Stub dependencies before loading the module
+(defvar cj/custom-keymap (make-sparse-keymap)
+ "Stub keymap for testing.")
+
+;; Now load the actual production module
+(require 'custom-misc)
+
+;;; Test Helpers
+
+(defun test-jump-to-matching-paren (text point-position)
+ "Test cj/jump-to-matching-paren with TEXT and point at POINT-POSITION.
+Returns the new point position after calling the function.
+POINT-POSITION is 1-indexed (1 = first character)."
+ (with-temp-buffer
+ (emacs-lisp-mode) ; Use elisp mode for proper syntax table
+ (insert text)
+ (goto-char point-position)
+ (cj/jump-to-matching-paren)
+ (point)))
+
+;;; Normal Cases - Forward Jump (Opening to Closing)
+
+(ert-deftest test-jump-paren-forward-simple ()
+ "Should jump forward from opening paren to closing paren."
+ ;; Text: "(hello)"
+ ;; Start at position 1 (on opening paren)
+ ;; Should end at position 8 (after closing paren)
+ (should (= 8 (test-jump-to-matching-paren "(hello)" 1))))
+
+(ert-deftest test-jump-paren-forward-nested ()
+ "Should jump forward over nested parens."
+ ;; Text: "(foo (bar))"
+ ;; Start at position 1 (on outer opening paren)
+ ;; Should end at position 12 (after outer closing paren)
+ (should (= 12 (test-jump-to-matching-paren "(foo (bar))" 1))))
+
+(ert-deftest test-jump-paren-forward-inner-nested ()
+ "Should jump forward from inner opening paren."
+ ;; Text: "(foo (bar))"
+ ;; Start at position 6 (on inner opening paren)
+ ;; Should end at position 11 (after inner closing paren)
+ (should (= 11 (test-jump-to-matching-paren "(foo (bar))" 6))))
+
+(ert-deftest test-jump-bracket-forward ()
+ "Should jump forward from opening bracket."
+ ;; Text: "[1 2 3]"
+ ;; Start at position 1
+ ;; Should end at position 8
+ (should (= 8 (test-jump-to-matching-paren "[1 2 3]" 1))))
+
+;; Note: Braces are not treated as matching delimiters in emacs-lisp-mode
+;; so we don't test them here
+
+;;; Normal Cases - Backward Jump (Closing to Opening)
+
+(ert-deftest test-jump-paren-backward-simple ()
+ "Should jump backward from closing paren to opening paren."
+ ;; Text: "(hello)"
+ ;; Start at position 7 (on closing paren)
+ ;; Should end at position 2 (after opening paren)
+ (should (= 2 (test-jump-to-matching-paren "(hello)" 7))))
+
+(ert-deftest test-jump-paren-backward-nested ()
+ "Should jump backward over nested parens from after outer closing."
+ ;; Text: "(foo (bar))"
+ ;; Start at position 12 (after outer closing paren)
+ ;; backward-sexp goes back to before opening paren
+ (should (= 1 (test-jump-to-matching-paren "(foo (bar))" 12))))
+
+(ert-deftest test-jump-paren-backward-inner-nested ()
+ "Should jump backward from inner closing paren."
+ ;; Text: "(foo (bar))"
+ ;; Start at position 10 (on inner closing paren)
+ ;; Should end at position 7 (after inner opening paren)
+ (should (= 7 (test-jump-to-matching-paren "(foo (bar))" 10))))
+
+(ert-deftest test-jump-bracket-backward ()
+ "Should jump backward from after closing bracket."
+ ;; Text: "[1 2 3]"
+ ;; Start at position 8 (after ])
+ ;; backward-sexp goes back one sexp
+ (should (= 1 (test-jump-to-matching-paren "[1 2 3]" 8))))
+
+;;; Normal Cases - Jump from After Closing Delimiter
+
+(ert-deftest test-jump-paren-after-closing ()
+ "Should jump backward when just after closing paren."
+ ;; Text: "(hello)"
+ ;; Start at position 8 (after closing paren)
+ ;; backward-sexp goes back one sexp, ending before the opening paren
+ (should (= 1 (test-jump-to-matching-paren "(hello)" 8))))
+
+;;; Boundary Cases - No Movement
+
+(ert-deftest test-jump-paren-not-on-delimiter ()
+ "Should not move when not on delimiter."
+ ;; Text: "(hello world)"
+ ;; Start at position 3 (on 'e' in hello)
+ ;; Should stay at position 3
+ (should (= 3 (test-jump-to-matching-paren "(hello world)" 3))))
+
+(ert-deftest test-jump-paren-on-whitespace ()
+ "Should not move when on whitespace."
+ ;; Text: "(hello world)"
+ ;; Start at position 7 (on space)
+ ;; Should stay at position 7
+ (should (= 7 (test-jump-to-matching-paren "(hello world)" 7))))
+
+;;; Boundary Cases - Unmatched Delimiters
+
+(ert-deftest test-jump-paren-unmatched-opening ()
+ "Should not move from unmatched opening paren."
+ ;; Text: "(hello"
+ ;; Start at position 1 (on opening paren with no closing)
+ ;; Should stay at position 1
+ (should (= 1 (test-jump-to-matching-paren "(hello" 1))))
+
+(ert-deftest test-jump-paren-unmatched-closing ()
+ "Should move to beginning from unmatched closing paren."
+ ;; Text: "hello)"
+ ;; Start at position 6 (on closing paren with no opening)
+ ;; backward-sexp with unmatched closing paren goes to beginning
+ (should (= 1 (test-jump-to-matching-paren "hello)" 6))))
+
+;;; Boundary Cases - Empty Delimiters
+
+(ert-deftest test-jump-paren-empty ()
+ "Should jump over empty parens."
+ ;; Text: "()"
+ ;; Start at position 1
+ ;; Should end at position 3
+ (should (= 3 (test-jump-to-matching-paren "()" 1))))
+
+(ert-deftest test-jump-paren-empty-backward ()
+ "Should stay put when on closing paren of empty parens."
+ ;; Text: "()"
+ ;; Start at position 2 (on closing paren)
+ ;; backward-sexp from closing of empty parens gives an error, so stays at 2
+ (should (= 2 (test-jump-to-matching-paren "()" 2))))
+
+;;; Boundary Cases - Multiple Delimiter Types
+
+(ert-deftest test-jump-paren-mixed-delimiters ()
+ "Should jump over mixed delimiter types."
+ ;; Text: "(foo [bar {baz}])"
+ ;; Start at position 1 (on opening paren)
+ ;; Should end at position 18 (after closing paren)
+ (should (= 18 (test-jump-to-matching-paren "(foo [bar {baz}])" 1))))
+
+(ert-deftest test-jump-bracket-in-parens ()
+ "Should jump from bracket inside parens."
+ ;; Text: "(foo [bar])"
+ ;; Start at position 6 (on opening bracket)
+ ;; Should end at position 11 (after closing bracket)
+ (should (= 11 (test-jump-to-matching-paren "(foo [bar])" 6))))
+
+;;; Complex Cases - Strings and Comments
+
+(ert-deftest test-jump-paren-over-string ()
+ "Should jump over parens containing strings."
+ ;; Text: "(\"hello (world)\")"
+ ;; Start at position 1 (on opening paren)
+ ;; Should end at position 18 (after closing paren)
+ ;; The parens in the string should be ignored
+ (should (= 18 (test-jump-to-matching-paren "(\"hello (world)\")" 1))))
+
+(provide 'test-custom-misc-jump-to-matching-paren)
+;;; test-custom-misc-jump-to-matching-paren.el ends here
diff --git a/tests/test-custom-misc-replace-fraction-glyphs.el b/tests/test-custom-misc-replace-fraction-glyphs.el
new file mode 100644
index 00000000..81d1546e
--- /dev/null
+++ b/tests/test-custom-misc-replace-fraction-glyphs.el
@@ -0,0 +1,185 @@
+;;; test-custom-misc-replace-fraction-glyphs.el --- Tests for cj/--replace-fraction-glyphs -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; Tests for the cj/--replace-fraction-glyphs function from custom-misc.el
+;;
+;; This function bidirectionally converts between text fractions (1/4) and
+;; Unicode fraction glyphs (¼). It supports 5 common fractions:
+;; - 1/4 ↔ ¼
+;; - 1/2 ↔ ½
+;; - 3/4 ↔ ¾
+;; - 1/3 ↔ ⅓
+;; - 2/3 ↔ ⅔
+;;
+;; We test the NON-INTERACTIVE implementation (cj/--replace-fraction-glyphs)
+;; to avoid mocking prefix arguments. This follows our testing best practice
+;; of separating business logic from UI interaction.
+
+;;; Code:
+
+(require 'ert)
+(require 'testutil-general)
+
+;; Add modules directory to load path
+(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory))
+
+;; Stub dependencies before loading the module
+(defvar cj/custom-keymap (make-sparse-keymap)
+ "Stub keymap for testing.")
+
+;; Now load the actual production module
+(require 'custom-misc)
+
+;;; Test Helpers
+
+(defun test-replace-fraction-glyphs (input-text to-glyphs)
+ "Test cj/--replace-fraction-glyphs on INPUT-TEXT.
+TO-GLYPHS determines conversion direction.
+Returns the buffer string after operation."
+ (with-temp-buffer
+ (insert input-text)
+ (cj/--replace-fraction-glyphs (point-min) (point-max) to-glyphs)
+ (buffer-string)))
+
+;;; Normal Cases - Text to Glyphs
+
+(ert-deftest test-replace-fraction-glyphs-text-to-glyph-quarter ()
+ "Should convert 1/4 to ¼."
+ (let ((result (test-replace-fraction-glyphs "1/4" t)))
+ (should (string= result "¼"))))
+
+(ert-deftest test-replace-fraction-glyphs-text-to-glyph-half ()
+ "Should convert 1/2 to ½."
+ (let ((result (test-replace-fraction-glyphs "1/2" t)))
+ (should (string= result "½"))))
+
+(ert-deftest test-replace-fraction-glyphs-text-to-glyph-three-quarters ()
+ "Should convert 3/4 to ¾."
+ (let ((result (test-replace-fraction-glyphs "3/4" t)))
+ (should (string= result "¾"))))
+
+(ert-deftest test-replace-fraction-glyphs-text-to-glyph-third ()
+ "Should convert 1/3 to ⅓."
+ (let ((result (test-replace-fraction-glyphs "1/3" t)))
+ (should (string= result "⅓"))))
+
+(ert-deftest test-replace-fraction-glyphs-text-to-glyph-two-thirds ()
+ "Should convert 2/3 to ⅔."
+ (let ((result (test-replace-fraction-glyphs "2/3" t)))
+ (should (string= result "⅔"))))
+
+(ert-deftest test-replace-fraction-glyphs-text-to-glyph-multiple ()
+ "Should convert multiple fractions in text."
+ (let ((result (test-replace-fraction-glyphs "Use 1/4 cup and 1/2 teaspoon" t)))
+ (should (string= result "Use ¼ cup and ½ teaspoon"))))
+
+(ert-deftest test-replace-fraction-glyphs-text-to-glyph-all-types ()
+ "Should convert all fraction types."
+ (let ((result (test-replace-fraction-glyphs "1/4 1/2 3/4 1/3 2/3" t)))
+ (should (string= result "¼ ½ ¾ ⅓ ⅔"))))
+
+;;; Normal Cases - Glyphs to Text
+
+(ert-deftest test-replace-fraction-glyphs-glyph-to-text-quarter ()
+ "Should convert ¼ to 1/4."
+ (let ((result (test-replace-fraction-glyphs "¼" nil)))
+ (should (string= result "1/4"))))
+
+(ert-deftest test-replace-fraction-glyphs-glyph-to-text-half ()
+ "Should convert ½ to 1/2."
+ (let ((result (test-replace-fraction-glyphs "½" nil)))
+ (should (string= result "1/2"))))
+
+(ert-deftest test-replace-fraction-glyphs-glyph-to-text-three-quarters ()
+ "Should convert ¾ to 3/4."
+ (let ((result (test-replace-fraction-glyphs "¾" nil)))
+ (should (string= result "3/4"))))
+
+(ert-deftest test-replace-fraction-glyphs-glyph-to-text-third ()
+ "Should convert ⅓ to 1/3."
+ (let ((result (test-replace-fraction-glyphs "⅓" nil)))
+ (should (string= result "1/3"))))
+
+(ert-deftest test-replace-fraction-glyphs-glyph-to-text-two-thirds ()
+ "Should convert ⅔ to 2/3."
+ (let ((result (test-replace-fraction-glyphs "⅔" nil)))
+ (should (string= result "2/3"))))
+
+(ert-deftest test-replace-fraction-glyphs-glyph-to-text-multiple ()
+ "Should convert multiple glyphs in text."
+ (let ((result (test-replace-fraction-glyphs "Use ¼ cup and ½ teaspoon" nil)))
+ (should (string= result "Use 1/4 cup and 1/2 teaspoon"))))
+
+(ert-deftest test-replace-fraction-glyphs-glyph-to-text-all-types ()
+ "Should convert all glyph types."
+ (let ((result (test-replace-fraction-glyphs "¼ ½ ¾ ⅓ ⅔" nil)))
+ (should (string= result "1/4 1/2 3/4 1/3 2/3"))))
+
+;;; Boundary Cases
+
+(ert-deftest test-replace-fraction-glyphs-empty-string ()
+ "Should handle empty string."
+ (let ((result (test-replace-fraction-glyphs "" t)))
+ (should (string= result ""))))
+
+(ert-deftest test-replace-fraction-glyphs-no-fractions-to-glyphs ()
+ "Should handle text with no fractions (no-op) when converting to glyphs."
+ (let ((result (test-replace-fraction-glyphs "hello world" t)))
+ (should (string= result "hello world"))))
+
+(ert-deftest test-replace-fraction-glyphs-no-fractions-to-text ()
+ "Should handle text with no glyphs (no-op) when converting to text."
+ (let ((result (test-replace-fraction-glyphs "hello world" nil)))
+ (should (string= result "hello world"))))
+
+(ert-deftest test-replace-fraction-glyphs-at-start ()
+ "Should handle fraction at start of text."
+ (let ((result (test-replace-fraction-glyphs "1/2 of the total" t)))
+ (should (string= result "½ of the total"))))
+
+(ert-deftest test-replace-fraction-glyphs-at-end ()
+ "Should handle fraction at end of text."
+ (let ((result (test-replace-fraction-glyphs "Reduce by 1/4" t)))
+ (should (string= result "Reduce by ¼"))))
+
+(ert-deftest test-replace-fraction-glyphs-repeated ()
+ "Should handle repeated fractions."
+ (let ((result (test-replace-fraction-glyphs "1/4 and 1/4 and 1/4" t)))
+ (should (string= result "¼ and ¼ and ¼"))))
+
+(ert-deftest test-replace-fraction-glyphs-very-long-text ()
+ "Should handle very long text with many fractions."
+ (let* ((long-text (mapconcat (lambda (_) "1/4") (make-list 50 nil) " "))
+ (result (test-replace-fraction-glyphs long-text t)))
+ (should (string-match-p "¼" result))
+ (should-not (string-match-p "1/4" result))))
+
+(ert-deftest test-replace-fraction-glyphs-bidirectional ()
+ "Should correctly convert back and forth."
+ (let* ((original "Use 1/4 cup")
+ (to-glyph (test-replace-fraction-glyphs original t))
+ (back-to-text (test-replace-fraction-glyphs to-glyph nil)))
+ (should (string= to-glyph "Use ¼ cup"))
+ (should (string= back-to-text original))))
+
+;;; Error Cases
+
+(ert-deftest test-replace-fraction-glyphs-start-greater-than-end ()
+ "Should error when start > end."
+ (should-error
+ (with-temp-buffer
+ (insert "1/4")
+ (cj/--replace-fraction-glyphs (point-max) (point-min) t))
+ :type 'error))
+
+(ert-deftest test-replace-fraction-glyphs-empty-region ()
+ "Should handle empty region (start == end) without error."
+ (with-temp-buffer
+ (insert "1/4")
+ (let ((pos (/ (+ (point-min) (point-max)) 2)))
+ (cj/--replace-fraction-glyphs pos pos t)
+ ;; Should complete without error
+ (should (string= (buffer-string) "1/4")))))
+
+(provide 'test-custom-misc-replace-fraction-glyphs)
+;;; test-custom-misc-replace-fraction-glyphs.el ends here
diff --git a/tests/test-custom-ordering-alphabetize.el b/tests/test-custom-ordering-alphabetize.el
new file mode 100644
index 00000000..c609e324
--- /dev/null
+++ b/tests/test-custom-ordering-alphabetize.el
@@ -0,0 +1,176 @@
+;;; test-custom-ordering-alphabetize.el --- Tests for cj/--alphabetize-region -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; Tests for the cj/--alphabetize-region function from custom-ordering.el
+;;
+;; This function alphabetically sorts words in a region.
+;; It splits by whitespace and commas, sorts alphabetically, and joins with ", ".
+;;
+;; Examples:
+;; Input: "zebra apple banana"
+;; Output: "apple, banana, zebra"
+;;
+;; Input: "dog, cat, bird"
+;; Output: "bird, cat, dog"
+;;
+;; We test the NON-INTERACTIVE implementation (cj/--alphabetize-region) to avoid
+;; mocking region selection. This follows our testing best practice of
+;; separating business logic from UI interaction.
+
+;;; Code:
+
+(require 'ert)
+(require 'testutil-general)
+
+;; Add modules directory to load path
+(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory))
+
+;; Stub dependencies before loading the module
+(defvar cj/custom-keymap (make-sparse-keymap)
+ "Stub keymap for testing.")
+
+;; Now load the actual production module
+(require 'custom-ordering)
+
+;;; Test Helpers
+
+(defun test-alphabetize (input-text)
+ "Test cj/--alphabetize-region on INPUT-TEXT.
+Returns the sorted, comma-separated string."
+ (with-temp-buffer
+ (insert input-text)
+ (cj/--alphabetize-region (point-min) (point-max))))
+
+;;; Normal Cases - Simple Words
+
+(ert-deftest test-alphabetize-simple-words ()
+ "Should alphabetize simple words."
+ (let ((result (test-alphabetize "zebra apple banana")))
+ (should (string= result "apple, banana, zebra"))))
+
+(ert-deftest test-alphabetize-already-sorted ()
+ "Should handle already sorted words."
+ (let ((result (test-alphabetize "apple banana cherry")))
+ (should (string= result "apple, banana, cherry"))))
+
+(ert-deftest test-alphabetize-reverse-order ()
+ "Should alphabetize reverse-ordered words."
+ (let ((result (test-alphabetize "zebra yankee xray")))
+ (should (string= result "xray, yankee, zebra"))))
+
+(ert-deftest test-alphabetize-two-words ()
+ "Should alphabetize two words."
+ (let ((result (test-alphabetize "world hello")))
+ (should (string= result "hello, world"))))
+
+;;; Normal Cases - With Commas
+
+(ert-deftest test-alphabetize-comma-separated ()
+ "Should alphabetize comma-separated words."
+ (let ((result (test-alphabetize "dog, cat, bird")))
+ (should (string= result "bird, cat, dog"))))
+
+(ert-deftest test-alphabetize-comma-separated-with-spaces ()
+ "Should handle comma-separated with various spacing."
+ (let ((result (test-alphabetize "dog,cat,bird")))
+ (should (string= result "bird, cat, dog"))))
+
+;;; Normal Cases - With Newlines
+
+(ert-deftest test-alphabetize-multiline ()
+ "Should alphabetize words across multiple lines."
+ (let ((result (test-alphabetize "zebra\napple\nbanana")))
+ (should (string= result "apple, banana, zebra"))))
+
+(ert-deftest test-alphabetize-mixed-separators ()
+ "Should alphabetize with mixed separators (spaces, commas, newlines)."
+ (let ((result (test-alphabetize "zebra, apple\nbanana cherry")))
+ (should (string= result "apple, banana, cherry, zebra"))))
+
+;;; Normal Cases - Case Sensitivity
+
+(ert-deftest test-alphabetize-case-sensitive ()
+ "Should sort case-sensitively (uppercase before lowercase)."
+ (let ((result (test-alphabetize "zebra Apple banana")))
+ ;; string-lessp sorts uppercase before lowercase
+ (should (string= result "Apple, banana, zebra"))))
+
+(ert-deftest test-alphabetize-mixed-case ()
+ "Should handle mixed case words."
+ (let ((result (test-alphabetize "ZEBRA apple BANANA")))
+ (should (string= result "BANANA, ZEBRA, apple"))))
+
+;;; Normal Cases - Numbers and Special Characters
+
+(ert-deftest test-alphabetize-with-numbers ()
+ "Should alphabetize numbers as strings."
+ (let ((result (test-alphabetize "10 2 1 20")))
+ ;; Alphabetic sort: "1", "10", "2", "20"
+ (should (string= result "1, 10, 2, 20"))))
+
+(ert-deftest test-alphabetize-mixed-alphanumeric ()
+ "Should alphabetize mixed alphanumeric content."
+ (let ((result (test-alphabetize "item2 item1 item10")))
+ (should (string= result "item1, item10, item2"))))
+
+(ert-deftest test-alphabetize-with-punctuation ()
+ "Should alphabetize words with punctuation."
+ (let ((result (test-alphabetize "world! hello? test.")))
+ (should (string= result "hello?, test., world!"))))
+
+;;; Boundary Cases
+
+(ert-deftest test-alphabetize-empty-string ()
+ "Should handle empty string."
+ (let ((result (test-alphabetize "")))
+ (should (string= result ""))))
+
+(ert-deftest test-alphabetize-single-word ()
+ "Should handle single word."
+ (let ((result (test-alphabetize "hello")))
+ (should (string= result "hello"))))
+
+(ert-deftest test-alphabetize-only-whitespace ()
+ "Should handle whitespace-only text."
+ (let ((result (test-alphabetize " \n\n\t\t ")))
+ (should (string= result ""))))
+
+(ert-deftest test-alphabetize-duplicates ()
+ "Should handle duplicate words."
+ (let ((result (test-alphabetize "apple banana apple cherry")))
+ (should (string= result "apple, apple, banana, cherry"))))
+
+(ert-deftest test-alphabetize-many-commas ()
+ "Should handle multiple consecutive commas."
+ (let ((result (test-alphabetize "apple,,,banana,,,cherry")))
+ (should (string= result "apple, banana, cherry"))))
+
+(ert-deftest test-alphabetize-very-long-list ()
+ "Should handle very long list."
+ (let* ((words (mapcar (lambda (i) (format "word%03d" i)) (number-sequence 100 1 -1)))
+ (input (mapconcat #'identity words " "))
+ (result (test-alphabetize input))
+ (sorted-words (split-string result ", ")))
+ (should (= 100 (length sorted-words)))
+ (should (string= "word001" (car sorted-words)))
+ (should (string= "word100" (car (last sorted-words))))))
+
+;;; Error Cases
+
+(ert-deftest test-alphabetize-start-greater-than-end ()
+ "Should error when start > end."
+ (should-error
+ (with-temp-buffer
+ (insert "hello world")
+ (cj/--alphabetize-region (point-max) (point-min)))
+ :type 'error))
+
+(ert-deftest test-alphabetize-empty-region ()
+ "Should handle empty region (start == end)."
+ (with-temp-buffer
+ (insert "hello world")
+ (let ((pos (/ (+ (point-min) (point-max)) 2)))
+ (should (string= "" (cj/--alphabetize-region pos pos))))))
+
+(provide 'test-custom-ordering-alphabetize)
+;;; test-custom-ordering-alphabetize.el ends here
diff --git a/tests/test-custom-ordering-arrayify.el b/tests/test-custom-ordering-arrayify.el
new file mode 100644
index 00000000..9aedbc46
--- /dev/null
+++ b/tests/test-custom-ordering-arrayify.el
@@ -0,0 +1,215 @@
+;;; test-custom-ordering-arrayify.el --- Tests for cj/--arrayify -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; Tests for the cj/--arrayify function from custom-ordering.el
+;;
+;; This function converts lines of text into a quoted, comma-separated array format.
+;; It splits input by whitespace, wraps each element in quotes, and joins with ", ".
+;;
+;; Examples:
+;; Input: "apple\nbanana\ncherry"
+;; Output: "\"apple\", \"banana\", \"cherry\""
+;;
+;; Input: "one two three" (with single quotes)
+;; Output: "'one', 'two', 'three'"
+;;
+;; We test the NON-INTERACTIVE implementation (cj/--arrayify) to avoid
+;; mocking user input for quote characters. This follows our testing best
+;; practice of separating business logic from UI interaction.
+
+;;; Code:
+
+(require 'ert)
+(require 'testutil-general)
+
+;; Add modules directory to load path
+(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory))
+
+;; Stub dependencies before loading the module
+(defvar cj/custom-keymap (make-sparse-keymap)
+ "Stub keymap for testing.")
+
+;; Now load the actual production module
+(require 'custom-ordering)
+
+;;; Test Helpers
+
+(defun test-arrayify (input-text quote)
+ "Test cj/--arrayify on INPUT-TEXT with QUOTE character.
+Returns the transformed string."
+ (with-temp-buffer
+ (insert input-text)
+ (cj/--arrayify (point-min) (point-max) quote)))
+
+(defun test-arrayify-with-prefix-suffix (input-text quote prefix suffix)
+ "Test cj/--arrayify with PREFIX and SUFFIX on INPUT-TEXT.
+Returns the transformed string."
+ (with-temp-buffer
+ (insert input-text)
+ (cj/--arrayify (point-min) (point-max) quote prefix suffix)))
+
+;;; Normal Cases - Double Quotes
+
+(ert-deftest test-arrayify-single-line-double-quotes ()
+ "Should arrayify single line with double quotes."
+ (let ((result (test-arrayify "apple banana cherry" "\"")))
+ (should (string= result "\"apple\", \"banana\", \"cherry\""))))
+
+(ert-deftest test-arrayify-multiple-lines-double-quotes ()
+ "Should arrayify multiple lines with double quotes."
+ (let ((result (test-arrayify "apple\nbanana\ncherry" "\"")))
+ (should (string= result "\"apple\", \"banana\", \"cherry\""))))
+
+(ert-deftest test-arrayify-mixed-whitespace-double-quotes ()
+ "Should arrayify text with mixed whitespace using double quotes."
+ (let ((result (test-arrayify "apple \n\n banana\t\tcherry" "\"")))
+ (should (string= result "\"apple\", \"banana\", \"cherry\""))))
+
+;;; Normal Cases - Single Quotes
+
+(ert-deftest test-arrayify-single-line-single-quotes ()
+ "Should arrayify single line with single quotes."
+ (let ((result (test-arrayify "one two three" "'")))
+ (should (string= result "'one', 'two', 'three'"))))
+
+(ert-deftest test-arrayify-multiple-lines-single-quotes ()
+ "Should arrayify multiple lines with single quotes."
+ (let ((result (test-arrayify "one\ntwo\nthree" "'")))
+ (should (string= result "'one', 'two', 'three'"))))
+
+;;; Normal Cases - Various Quote Types
+
+(ert-deftest test-arrayify-backticks ()
+ "Should arrayify with backticks."
+ (let ((result (test-arrayify "foo bar baz" "`")))
+ (should (string= result "`foo`, `bar`, `baz`"))))
+
+(ert-deftest test-arrayify-no-quotes ()
+ "Should arrayify with empty quote string."
+ (let ((result (test-arrayify "alpha beta gamma" "")))
+ (should (string= result "alpha, beta, gamma"))))
+
+(ert-deftest test-arrayify-square-brackets ()
+ "Should arrayify with square brackets as quotes."
+ (let ((result (test-arrayify "x y z" "[]")))
+ (should (string= result "[]x[], []y[], []z[]"))))
+
+;;; Normal Cases - Various Content
+
+(ert-deftest test-arrayify-with-numbers ()
+ "Should arrayify numbers."
+ (let ((result (test-arrayify "1 2 3 4 5" "\"")))
+ (should (string= result "\"1\", \"2\", \"3\", \"4\", \"5\""))))
+
+(ert-deftest test-arrayify-with-punctuation ()
+ "Should arrayify words with punctuation."
+ (let ((result (test-arrayify "hello! world? test." "\"")))
+ (should (string= result "\"hello!\", \"world?\", \"test.\""))))
+
+(ert-deftest test-arrayify-mixed-content ()
+ "Should arrayify mixed alphanumeric content."
+ (let ((result (test-arrayify "item1 item2 item3" "\"")))
+ (should (string= result "\"item1\", \"item2\", \"item3\""))))
+
+;;; Boundary Cases
+
+(ert-deftest test-arrayify-empty-string ()
+ "Should handle empty string."
+ (let ((result (test-arrayify "" "\"")))
+ (should (string= result ""))))
+
+(ert-deftest test-arrayify-single-word ()
+ "Should arrayify single word."
+ (let ((result (test-arrayify "hello" "\"")))
+ (should (string= result "\"hello\""))))
+
+(ert-deftest test-arrayify-only-whitespace ()
+ "Should handle whitespace-only text."
+ (let ((result (test-arrayify " \n\n\t\t " "\"")))
+ (should (string= result ""))))
+
+(ert-deftest test-arrayify-leading-trailing-whitespace ()
+ "Should ignore leading and trailing whitespace."
+ (let ((result (test-arrayify " apple banana " "\"")))
+ (should (string= result "\"apple\", \"banana\""))))
+
+(ert-deftest test-arrayify-very-long-list ()
+ "Should handle very long list."
+ (let* ((words (make-list 100 "word"))
+ (input (mapconcat #'identity words " "))
+ (result (test-arrayify input "\"")))
+ (should (= 100 (length (split-string result ", "))))))
+
+(ert-deftest test-arrayify-two-words ()
+ "Should arrayify two words."
+ (let ((result (test-arrayify "hello world" "\"")))
+ (should (string= result "\"hello\", \"world\""))))
+
+;;; Normal Cases - Prefix/Suffix
+
+(ert-deftest test-arrayify-with-square-brackets ()
+ "Should arrayify with square brackets prefix/suffix."
+ (let ((result (test-arrayify-with-prefix-suffix "apple banana cherry" "\"" "[" "]")))
+ (should (string= result "[\"apple\", \"banana\", \"cherry\"]"))))
+
+(ert-deftest test-arrayify-with-parens ()
+ "Should arrayify with parentheses prefix/suffix."
+ (let ((result (test-arrayify-with-prefix-suffix "one two three" "\"" "(" ")")))
+ (should (string= result "(\"one\", \"two\", \"three\")"))))
+
+(ert-deftest test-arrayify-unquoted-with-brackets ()
+ "Should create unquoted list with brackets."
+ (let ((result (test-arrayify-with-prefix-suffix "a b c" "" "[" "]")))
+ (should (string= result "[a, b, c]"))))
+
+(ert-deftest test-arrayify-single-quotes-with-brackets ()
+ "Should create single-quoted array with brackets."
+ (let ((result (test-arrayify-with-prefix-suffix "x y z" "'" "[" "]")))
+ (should (string= result "['x', 'y', 'z']"))))
+
+(ert-deftest test-arrayify-only-prefix ()
+ "Should handle only prefix, no suffix."
+ (let ((result (test-arrayify-with-prefix-suffix "foo bar" "\"" "[" nil)))
+ (should (string= result "[\"foo\", \"bar\""))))
+
+(ert-deftest test-arrayify-only-suffix ()
+ "Should handle only suffix, no prefix."
+ (let ((result (test-arrayify-with-prefix-suffix "foo bar" "\"" nil "]")))
+ (should (string= result "\"foo\", \"bar\"]"))))
+
+(ert-deftest test-arrayify-multichar-prefix-suffix ()
+ "Should handle multi-character prefix/suffix."
+ (let ((result (test-arrayify-with-prefix-suffix "a b" "\"" "Array(" ")")))
+ (should (string= result "Array(\"a\", \"b\")"))))
+
+(ert-deftest test-arrayify-json-style ()
+ "Should create JSON-style array."
+ (let ((result (test-arrayify-with-prefix-suffix "apple banana" "\"" "[" "]")))
+ (should (string= result "[\"apple\", \"banana\"]"))))
+
+;;; Error Cases
+
+(ert-deftest test-arrayify-start-greater-than-end ()
+ "Should error when start > end."
+ (should-error
+ (with-temp-buffer
+ (insert "hello world")
+ (cj/--arrayify (point-max) (point-min) "\""))
+ :type 'error))
+
+(ert-deftest test-arrayify-empty-region ()
+ "Should handle empty region (start == end)."
+ (with-temp-buffer
+ (insert "hello world")
+ (let ((pos (/ (+ (point-min) (point-max)) 2)))
+ (should (string= "" (cj/--arrayify pos pos "\""))))))
+
+(ert-deftest test-arrayify-empty-region-with-brackets ()
+ "Should handle empty region with brackets."
+ (with-temp-buffer
+ (insert "hello world")
+ (let ((pos (/ (+ (point-min) (point-max)) 2)))
+ (should (string= "[]" (cj/--arrayify pos pos "\"" "[" "]"))))))
+
+(provide 'test-custom-ordering-arrayify)
+;;; test-custom-ordering-arrayify.el ends here
diff --git a/tests/test-custom-ordering-comma-to-lines.el b/tests/test-custom-ordering-comma-to-lines.el
new file mode 100644
index 00000000..93e37ec6
--- /dev/null
+++ b/tests/test-custom-ordering-comma-to-lines.el
@@ -0,0 +1,159 @@
+;;; test-custom-ordering-comma-to-lines.el --- Tests for cj/--comma-separated-text-to-lines -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; Tests for the cj/--comma-separated-text-to-lines function from custom-ordering.el
+;;
+;; This function converts comma-separated text to separate lines.
+;; It replaces commas with newlines and removes trailing whitespace from each line.
+;;
+;; Examples:
+;; Input: "apple, banana, cherry"
+;; Output: "apple\nbanana\ncherry"
+;;
+;; Input: "one,two,three"
+;; Output: "one\ntwo\nthree"
+;;
+;; We test the NON-INTERACTIVE implementation (cj/--comma-separated-text-to-lines)
+;; to avoid mocking region selection. This follows our testing best practice of
+;; separating business logic from UI interaction.
+
+;;; Code:
+
+(require 'ert)
+(require 'testutil-general)
+
+;; Add modules directory to load path
+(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory))
+
+;; Stub dependencies before loading the module
+(defvar cj/custom-keymap (make-sparse-keymap)
+ "Stub keymap for testing.")
+
+;; Now load the actual production module
+(require 'custom-ordering)
+
+;;; Test Helpers
+
+(defun test-comma-to-lines (input-text)
+ "Test cj/--comma-separated-text-to-lines on INPUT-TEXT.
+Returns the transformed string."
+ (with-temp-buffer
+ (insert input-text)
+ (cj/--comma-separated-text-to-lines (point-min) (point-max))))
+
+;;; Normal Cases - Simple Comma-Separated
+
+(ert-deftest test-comma-to-lines-simple ()
+ "Should convert simple comma-separated text to lines."
+ (let ((result (test-comma-to-lines "apple, banana, cherry")))
+ (should (string= result "apple\n banana\n cherry"))))
+
+(ert-deftest test-comma-to-lines-no-spaces ()
+ "Should convert comma-separated text without spaces."
+ (let ((result (test-comma-to-lines "one,two,three")))
+ (should (string= result "one\ntwo\nthree"))))
+
+(ert-deftest test-comma-to-lines-two-elements ()
+ "Should convert two comma-separated elements."
+ (let ((result (test-comma-to-lines "hello,world")))
+ (should (string= result "hello\nworld"))))
+
+(ert-deftest test-comma-to-lines-with-varied-spacing ()
+ "Should preserve leading spaces after commas."
+ (let ((result (test-comma-to-lines "alpha, beta, gamma")))
+ (should (string= result "alpha\n beta\n gamma"))))
+
+;;; Normal Cases - Trailing Whitespace
+
+(ert-deftest test-comma-to-lines-trailing-spaces ()
+ "Should remove trailing spaces but preserve leading spaces."
+ (let ((result (test-comma-to-lines "apple , banana , cherry ")))
+ (should (string= result "apple\n banana\n cherry"))))
+
+(ert-deftest test-comma-to-lines-trailing-tabs ()
+ "Should remove trailing tabs after conversion."
+ (let ((result (test-comma-to-lines "apple\t,banana\t,cherry\t")))
+ (should (string= result "apple\nbanana\ncherry"))))
+
+;;; Boundary Cases
+
+(ert-deftest test-comma-to-lines-empty-string ()
+ "Should handle empty string."
+ (let ((result (test-comma-to-lines "")))
+ (should (string= result ""))))
+
+(ert-deftest test-comma-to-lines-single-element ()
+ "Should handle single element with no comma."
+ (let ((result (test-comma-to-lines "hello")))
+ (should (string= result "hello"))))
+
+(ert-deftest test-comma-to-lines-single-element-with-trailing-comma ()
+ "Should handle single element with trailing comma."
+ (let ((result (test-comma-to-lines "hello,")))
+ (should (string= result "hello\n"))))
+
+(ert-deftest test-comma-to-lines-leading-comma ()
+ "Should handle leading comma."
+ (let ((result (test-comma-to-lines ",apple,banana")))
+ (should (string= result "\napple\nbanana"))))
+
+(ert-deftest test-comma-to-lines-consecutive-commas ()
+ "Should handle consecutive commas."
+ (let ((result (test-comma-to-lines "apple,,banana")))
+ (should (string= result "apple\n\nbanana"))))
+
+(ert-deftest test-comma-to-lines-many-consecutive-commas ()
+ "Should handle many consecutive commas."
+ (let ((result (test-comma-to-lines "apple,,,banana")))
+ (should (string= result "apple\n\n\nbanana"))))
+
+(ert-deftest test-comma-to-lines-only-commas ()
+ "Should handle string with only commas (trailing blank lines removed)."
+ (let ((result (test-comma-to-lines ",,,")))
+ ;; delete-trailing-whitespace removes trailing blank lines
+ (should (string= result "\n"))))
+
+;;; Normal Cases - With Spaces Around Elements
+
+(ert-deftest test-comma-to-lines-leading-spaces ()
+ "Should preserve leading spaces within elements."
+ (let ((result (test-comma-to-lines " apple, banana, cherry")))
+ (should (string= result " apple\n banana\n cherry"))))
+
+(ert-deftest test-comma-to-lines-mixed-content ()
+ "Should handle mixed alphanumeric content."
+ (let ((result (test-comma-to-lines "item1,item2,item3")))
+ (should (string= result "item1\nitem2\nitem3"))))
+
+(ert-deftest test-comma-to-lines-with-numbers ()
+ "Should handle numbers."
+ (let ((result (test-comma-to-lines "1,2,3,4,5")))
+ (should (string= result "1\n2\n3\n4\n5"))))
+
+(ert-deftest test-comma-to-lines-very-long-list ()
+ "Should handle very long list."
+ (let* ((elements (mapcar #'number-to-string (number-sequence 1 100)))
+ (input (mapconcat #'identity elements ","))
+ (result (test-comma-to-lines input))
+ (lines (split-string result "\n")))
+ (should (= 100 (length lines)))))
+
+;;; Error Cases
+
+(ert-deftest test-comma-to-lines-start-greater-than-end ()
+ "Should error when start > end."
+ (should-error
+ (with-temp-buffer
+ (insert "a,b,c")
+ (cj/--comma-separated-text-to-lines (point-max) (point-min)))
+ :type 'error))
+
+(ert-deftest test-comma-to-lines-empty-region ()
+ "Should handle empty region (start == end)."
+ (with-temp-buffer
+ (insert "a,b,c")
+ (let ((pos (/ (+ (point-min) (point-max)) 2)))
+ (should (string= "" (cj/--comma-separated-text-to-lines pos pos))))))
+
+(provide 'test-custom-ordering-comma-to-lines)
+;;; test-custom-ordering-comma-to-lines.el ends here
diff --git a/tests/test-custom-ordering-number-lines.el b/tests/test-custom-ordering-number-lines.el
new file mode 100644
index 00000000..adda84f0
--- /dev/null
+++ b/tests/test-custom-ordering-number-lines.el
@@ -0,0 +1,181 @@
+;;; test-custom-ordering-number-lines.el --- Tests for cj/--number-lines -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; Tests for the cj/--number-lines function from custom-ordering.el
+;;
+;; This function numbers lines in a region with a customizable format.
+;; The format string uses "N" as a placeholder for the line number.
+;; Optionally supports zero-padding for alignment.
+;;
+;; Examples:
+;; Input: "apple\nbanana\ncherry"
+;; Format: "N. "
+;; Output: "1. apple\n2. banana\n3. cherry"
+;;
+;; With zero-padding and 100 lines:
+;; "001. line\n002. line\n...\n100. line"
+;;
+;; We test the NON-INTERACTIVE implementation (cj/--number-lines) to avoid
+;; mocking user input. This follows our testing best practice of
+;; separating business logic from UI interaction.
+
+;;; Code:
+
+(require 'ert)
+(require 'testutil-general)
+(require 'cl-lib)
+
+;; Add modules directory to load path
+(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory))
+
+;; Stub dependencies before loading the module
+(defvar cj/custom-keymap (make-sparse-keymap)
+ "Stub keymap for testing.")
+
+;; Now load the actual production module
+(require 'custom-ordering)
+
+;;; Test Helpers
+
+(defun test-number-lines (input-text format-string zero-pad)
+ "Test cj/--number-lines on INPUT-TEXT.
+FORMAT-STRING is the format template.
+ZERO-PAD enables zero-padding.
+Returns the transformed string."
+ (with-temp-buffer
+ (insert input-text)
+ (cj/--number-lines (point-min) (point-max) format-string zero-pad)))
+
+;;; Normal Cases - Standard Format "N. "
+
+(ert-deftest test-number-lines-standard-format ()
+ "Should number lines with standard format."
+ (let ((result (test-number-lines "apple\nbanana\ncherry" "N. " nil)))
+ (should (string= result "1. apple\n2. banana\n3. cherry"))))
+
+(ert-deftest test-number-lines-two-lines ()
+ "Should number two lines."
+ (let ((result (test-number-lines "first\nsecond" "N. " nil)))
+ (should (string= result "1. first\n2. second"))))
+
+(ert-deftest test-number-lines-single-line ()
+ "Should number single line."
+ (let ((result (test-number-lines "only" "N. " nil)))
+ (should (string= result "1. only"))))
+
+;;; Normal Cases - Alternative Formats
+
+(ert-deftest test-number-lines-parenthesis-format ()
+ "Should number with parenthesis format."
+ (let ((result (test-number-lines "a\nb\nc" "N) " nil)))
+ (should (string= result "1) a\n2) b\n3) c"))))
+
+(ert-deftest test-number-lines-bracket-format ()
+ "Should number with bracket format."
+ (let ((result (test-number-lines "x\ny\nz" "[N] " nil)))
+ (should (string= result "[1] x\n[2] y\n[3] z"))))
+
+(ert-deftest test-number-lines-no-space-format ()
+ "Should number without space."
+ (let ((result (test-number-lines "a\nb" "N." nil)))
+ (should (string= result "1.a\n2.b"))))
+
+(ert-deftest test-number-lines-custom-format ()
+ "Should number with custom format."
+ (let ((result (test-number-lines "foo\nbar" "Item N: " nil)))
+ (should (string= result "Item 1: foo\nItem 2: bar"))))
+
+;;; Normal Cases - Zero Padding
+
+(ert-deftest test-number-lines-zero-pad-single-digit ()
+ "Should not pad when max is single digit."
+ (let ((result (test-number-lines "a\nb\nc" "N. " t)))
+ (should (string= result "1. a\n2. b\n3. c"))))
+
+(ert-deftest test-number-lines-zero-pad-double-digit ()
+ "Should pad to 2 digits when max is 10-99."
+ (let* ((lines (make-list 12 "line"))
+ (input (mapconcat #'identity lines "\n"))
+ (result (test-number-lines input "N. " t))
+ (result-lines (split-string result "\n")))
+ (should (string-prefix-p "01. " (nth 0 result-lines)))
+ (should (string-prefix-p "09. " (nth 8 result-lines)))
+ (should (string-prefix-p "10. " (nth 9 result-lines)))
+ (should (string-prefix-p "12. " (nth 11 result-lines)))))
+
+(ert-deftest test-number-lines-zero-pad-triple-digit ()
+ "Should pad to 3 digits when max is 100+."
+ (let* ((lines (make-list 105 "x"))
+ (input (mapconcat #'identity lines "\n"))
+ (result (test-number-lines input "N. " t))
+ (result-lines (split-string result "\n")))
+ (should (string-prefix-p "001. " (nth 0 result-lines)))
+ (should (string-prefix-p "099. " (nth 98 result-lines)))
+ (should (string-prefix-p "100. " (nth 99 result-lines)))
+ (should (string-prefix-p "105. " (nth 104 result-lines)))))
+
+;;; Boundary Cases
+
+(ert-deftest test-number-lines-empty-string ()
+ "Should handle empty string."
+ (let ((result (test-number-lines "" "N. " nil)))
+ (should (string= result "1. "))))
+
+(ert-deftest test-number-lines-empty-lines ()
+ "Should number empty lines."
+ (let ((result (test-number-lines "\n\n" "N. " nil)))
+ (should (string= result "1. \n2. \n3. "))))
+
+(ert-deftest test-number-lines-with-existing-numbers ()
+ "Should number lines that already have content."
+ (let ((result (test-number-lines "1. old\n2. old" "N. " nil)))
+ (should (string= result "1. 1. old\n2. 2. old"))))
+
+(ert-deftest test-number-lines-multiple-N-in-format ()
+ "Should replace multiple N occurrences."
+ (let ((result (test-number-lines "a\nb" "N-N. " nil)))
+ (should (string= result "1-1. a\n2-2. b"))))
+
+(ert-deftest test-number-lines-long-content ()
+ "Should number lines with long content."
+ (let* ((long-line (make-string 100 ?x))
+ (input (format "%s\n%s" long-line long-line))
+ (result (test-number-lines input "N. " nil)))
+ (should (string-prefix-p "1. " result))
+ (should (string-match "2\\. " result))))
+
+;;; Normal Cases - No Zero Padding vs Zero Padding
+
+(ert-deftest test-number-lines-comparison-no-pad-vs-pad ()
+ "Should show difference between no padding and padding."
+ (let* ((input "a\nb\nc\nd\ne\nf\ng\nh\ni\nj")
+ (no-pad (test-number-lines input "N. " nil))
+ (with-pad (test-number-lines input "N. " t))
+ (no-pad-lines (split-string no-pad "\n"))
+ (with-pad-lines (split-string with-pad "\n")))
+ ;; Without padding: "1. ", "10. "
+ (should (string-prefix-p "1. " (nth 0 no-pad-lines)))
+ (should (string-prefix-p "10. " (nth 9 no-pad-lines)))
+ ;; With padding: "01. ", "10. "
+ (should (string-prefix-p "01. " (nth 0 with-pad-lines)))
+ (should (string-prefix-p "10. " (nth 9 with-pad-lines)))))
+
+;;; Error Cases
+
+(ert-deftest test-number-lines-start-greater-than-end ()
+ "Should error when start > end."
+ (should-error
+ (with-temp-buffer
+ (insert "line1\nline2")
+ (cj/--number-lines (point-max) (point-min) "N. " nil))
+ :type 'error))
+
+(ert-deftest test-number-lines-empty-region ()
+ "Should handle empty region (start == end)."
+ (with-temp-buffer
+ (insert "line1\nline2")
+ (let ((pos (/ (+ (point-min) (point-max)) 2)))
+ (should (string= "1. " (cj/--number-lines pos pos "N. " nil))))))
+
+(provide 'test-custom-ordering-number-lines)
+;;; test-custom-ordering-number-lines.el ends here
diff --git a/tests/test-custom-ordering-reverse-lines.el b/tests/test-custom-ordering-reverse-lines.el
new file mode 100644
index 00000000..3c71362d
--- /dev/null
+++ b/tests/test-custom-ordering-reverse-lines.el
@@ -0,0 +1,131 @@
+;;; test-custom-ordering-reverse-lines.el --- Tests for cj/--reverse-lines -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; Tests for the cj/--reverse-lines function from custom-ordering.el
+;;
+;; This function reverses the order of lines in a region.
+;; The first line becomes last, last becomes first, etc.
+;;
+;; Examples:
+;; Input: "line1\nline2\nline3"
+;; Output: "line3\nline2\nline1"
+;;
+;; We test the NON-INTERACTIVE implementation (cj/--reverse-lines) to avoid
+;; mocking region selection. This follows our testing best practice of
+;; separating business logic from UI interaction.
+
+;;; Code:
+
+(require 'ert)
+(require 'testutil-general)
+
+;; Add modules directory to load path
+(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory))
+
+;; Stub dependencies before loading the module
+(defvar cj/custom-keymap (make-sparse-keymap)
+ "Stub keymap for testing.")
+
+;; Now load the actual production module
+(require 'custom-ordering)
+
+;;; Test Helpers
+
+(defun test-reverse-lines (input-text)
+ "Test cj/--reverse-lines on INPUT-TEXT.
+Returns the transformed string."
+ (with-temp-buffer
+ (insert input-text)
+ (cj/--reverse-lines (point-min) (point-max))))
+
+;;; Normal Cases
+
+(ert-deftest test-reverse-lines-three-lines ()
+ "Should reverse three lines."
+ (let ((result (test-reverse-lines "line1\nline2\nline3")))
+ (should (string= result "line3\nline2\nline1"))))
+
+(ert-deftest test-reverse-lines-two-lines ()
+ "Should reverse two lines."
+ (let ((result (test-reverse-lines "first\nsecond")))
+ (should (string= result "second\nfirst"))))
+
+(ert-deftest test-reverse-lines-many-lines ()
+ "Should reverse many lines."
+ (let ((result (test-reverse-lines "a\nb\nc\nd\ne")))
+ (should (string= result "e\nd\nc\nb\na"))))
+
+(ert-deftest test-reverse-lines-with-content ()
+ "Should reverse lines with actual content."
+ (let ((result (test-reverse-lines "apple banana\ncherry date\negg fig")))
+ (should (string= result "egg fig\ncherry date\napple banana"))))
+
+(ert-deftest test-reverse-lines-bidirectional ()
+ "Should reverse back and forth correctly."
+ (let* ((original "line1\nline2\nline3")
+ (reversed (test-reverse-lines original))
+ (back (test-reverse-lines reversed)))
+ (should (string= reversed "line3\nline2\nline1"))
+ (should (string= back original))))
+
+;;; Boundary Cases
+
+(ert-deftest test-reverse-lines-empty-string ()
+ "Should handle empty string."
+ (let ((result (test-reverse-lines "")))
+ (should (string= result ""))))
+
+(ert-deftest test-reverse-lines-single-line ()
+ "Should handle single line (no change)."
+ (let ((result (test-reverse-lines "single line")))
+ (should (string= result "single line"))))
+
+(ert-deftest test-reverse-lines-empty-lines ()
+ "Should reverse including empty lines."
+ (let ((result (test-reverse-lines "a\n\nb")))
+ (should (string= result "b\n\na"))))
+
+(ert-deftest test-reverse-lines-trailing-newline ()
+ "Should handle trailing newline."
+ (let ((result (test-reverse-lines "line1\nline2\n")))
+ (should (string= result "\nline2\nline1"))))
+
+(ert-deftest test-reverse-lines-only-newlines ()
+ "Should reverse lines that are only newlines."
+ (let ((result (test-reverse-lines "\n\n\n")))
+ (should (string= result "\n\n\n"))))
+
+(ert-deftest test-reverse-lines-numbers ()
+ "Should reverse numbered lines."
+ (let ((result (test-reverse-lines "1\n2\n3\n4\n5")))
+ (should (string= result "5\n4\n3\n2\n1"))))
+
+(ert-deftest test-reverse-lines-very-long ()
+ "Should reverse very long list."
+ (let* ((lines (mapcar #'number-to-string (number-sequence 1 100)))
+ (input (mapconcat #'identity lines "\n"))
+ (result (test-reverse-lines input))
+ (result-lines (split-string result "\n")))
+ (should (= 100 (length result-lines)))
+ (should (string= "100" (car result-lines)))
+ (should (string= "1" (car (last result-lines))))))
+
+;;; Error Cases
+
+(ert-deftest test-reverse-lines-start-greater-than-end ()
+ "Should error when start > end."
+ (should-error
+ (with-temp-buffer
+ (insert "line1\nline2")
+ (cj/--reverse-lines (point-max) (point-min)))
+ :type 'error))
+
+(ert-deftest test-reverse-lines-empty-region ()
+ "Should handle empty region (start == end)."
+ (with-temp-buffer
+ (insert "line1\nline2")
+ (let ((pos (/ (+ (point-min) (point-max)) 2)))
+ (should (string= "" (cj/--reverse-lines pos pos))))))
+
+(provide 'test-custom-ordering-reverse-lines)
+;;; test-custom-ordering-reverse-lines.el ends here
diff --git a/tests/test-custom-ordering-toggle-quotes.el b/tests/test-custom-ordering-toggle-quotes.el
new file mode 100644
index 00000000..e11305ee
--- /dev/null
+++ b/tests/test-custom-ordering-toggle-quotes.el
@@ -0,0 +1,155 @@
+;;; test-custom-ordering-toggle-quotes.el --- Tests for cj/--toggle-quotes -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; Tests for the cj/--toggle-quotes function from custom-ordering.el
+;;
+;; This function toggles between double quotes and single quotes.
+;; All " become ' and all ' become ".
+;;
+;; Examples:
+;; Input: "apple", "banana"
+;; Output: 'apple', 'banana'
+;;
+;; Input: 'hello', 'world'
+;; Output: "hello", "world"
+;;
+;; We test the NON-INTERACTIVE implementation (cj/--toggle-quotes) to avoid
+;; mocking region selection. This follows our testing best practice of
+;; separating business logic from UI interaction.
+
+;;; Code:
+
+(require 'ert)
+(require 'testutil-general)
+
+;; Add modules directory to load path
+(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory))
+
+;; Stub dependencies before loading the module
+(defvar cj/custom-keymap (make-sparse-keymap)
+ "Stub keymap for testing.")
+
+;; Now load the actual production module
+(require 'custom-ordering)
+
+;;; Test Helpers
+
+(defun test-toggle-quotes (input-text)
+ "Test cj/--toggle-quotes on INPUT-TEXT.
+Returns the transformed string."
+ (with-temp-buffer
+ (insert input-text)
+ (cj/--toggle-quotes (point-min) (point-max))))
+
+;;; Normal Cases - Double to Single
+
+(ert-deftest test-toggle-quotes-double-to-single ()
+ "Should convert double quotes to single quotes."
+ (let ((result (test-toggle-quotes "\"apple\", \"banana\"")))
+ (should (string= result "'apple', 'banana'"))))
+
+(ert-deftest test-toggle-quotes-single-double-quote ()
+ "Should convert single double quote."
+ (let ((result (test-toggle-quotes "\"")))
+ (should (string= result "'"))))
+
+(ert-deftest test-toggle-quotes-multiple-double-quotes ()
+ "Should convert multiple double quotes."
+ (let ((result (test-toggle-quotes "\"hello\" \"world\" \"test\"")))
+ (should (string= result "'hello' 'world' 'test'"))))
+
+;;; Normal Cases - Single to Double
+
+(ert-deftest test-toggle-quotes-single-to-double ()
+ "Should convert single quotes to double quotes."
+ (let ((result (test-toggle-quotes "'apple', 'banana'")))
+ (should (string= result "\"apple\", \"banana\""))))
+
+(ert-deftest test-toggle-quotes-single-single-quote ()
+ "Should convert single single quote."
+ (let ((result (test-toggle-quotes "'")))
+ (should (string= result "\""))))
+
+(ert-deftest test-toggle-quotes-multiple-single-quotes ()
+ "Should convert multiple single quotes."
+ (let ((result (test-toggle-quotes "'hello' 'world' 'test'")))
+ (should (string= result "\"hello\" \"world\" \"test\""))))
+
+;;; Normal Cases - Mixed Quotes
+
+(ert-deftest test-toggle-quotes-mixed ()
+ "Should toggle mixed quotes."
+ (let ((result (test-toggle-quotes "\"double\" 'single'")))
+ (should (string= result "'double' \"single\""))))
+
+(ert-deftest test-toggle-quotes-bidirectional ()
+ "Should toggle back and forth correctly."
+ (let* ((original "\"apple\", \"banana\"")
+ (toggled (test-toggle-quotes original))
+ (back (test-toggle-quotes toggled)))
+ (should (string= toggled "'apple', 'banana'"))
+ (should (string= back original))))
+
+;;; Normal Cases - With Text Content
+
+(ert-deftest test-toggle-quotes-preserves-content ()
+ "Should preserve content while toggling quotes."
+ (let ((result (test-toggle-quotes "var x = \"hello world\";")))
+ (should (string= result "var x = 'hello world';"))))
+
+(ert-deftest test-toggle-quotes-sql-style ()
+ "Should toggle SQL-style quotes."
+ (let ((result (test-toggle-quotes "SELECT * FROM users WHERE name='John'")))
+ (should (string= result "SELECT * FROM users WHERE name=\"John\""))))
+
+(ert-deftest test-toggle-quotes-multiline ()
+ "Should toggle quotes across multiple lines."
+ (let ((result (test-toggle-quotes "\"line1\"\n\"line2\"\n\"line3\"")))
+ (should (string= result "'line1'\n'line2'\n'line3'"))))
+
+;;; Boundary Cases
+
+(ert-deftest test-toggle-quotes-empty-string ()
+ "Should handle empty string."
+ (let ((result (test-toggle-quotes "")))
+ (should (string= result ""))))
+
+(ert-deftest test-toggle-quotes-no-quotes ()
+ "Should handle text with no quotes."
+ (let ((result (test-toggle-quotes "hello world")))
+ (should (string= result "hello world"))))
+
+(ert-deftest test-toggle-quotes-only-double-quotes ()
+ "Should handle string with only double quotes."
+ (let ((result (test-toggle-quotes "\"\"\"\"")))
+ (should (string= result "''''"))))
+
+(ert-deftest test-toggle-quotes-only-single-quotes ()
+ "Should handle string with only single quotes."
+ (let ((result (test-toggle-quotes "''''")))
+ (should (string= result "\"\"\"\""))))
+
+(ert-deftest test-toggle-quotes-adjacent-quotes ()
+ "Should handle adjacent quotes."
+ (let ((result (test-toggle-quotes "\"\"''")))
+ (should (string= result "''\"\""))))
+
+;;; Error Cases
+
+(ert-deftest test-toggle-quotes-start-greater-than-end ()
+ "Should error when start > end."
+ (should-error
+ (with-temp-buffer
+ (insert "\"hello\"")
+ (cj/--toggle-quotes (point-max) (point-min)))
+ :type 'error))
+
+(ert-deftest test-toggle-quotes-empty-region ()
+ "Should handle empty region (start == end)."
+ (with-temp-buffer
+ (insert "\"hello\"")
+ (let ((pos (/ (+ (point-min) (point-max)) 2)))
+ (should (string= "" (cj/--toggle-quotes pos pos))))))
+
+(provide 'test-custom-ordering-toggle-quotes)
+;;; test-custom-ordering-toggle-quotes.el ends here
diff --git a/tests/test-custom-ordering-unarrayify.el b/tests/test-custom-ordering-unarrayify.el
new file mode 100644
index 00000000..a778f419
--- /dev/null
+++ b/tests/test-custom-ordering-unarrayify.el
@@ -0,0 +1,159 @@
+;;; test-custom-ordering-unarrayify.el --- Tests for cj/--unarrayify -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; Tests for the cj/--unarrayify function from custom-ordering.el
+;;
+;; This function converts comma-separated array format back to separate lines.
+;; It splits by ", " (comma-space), removes quotes (both " and '), and joins with newlines.
+;;
+;; Examples:
+;; Input: "\"apple\", \"banana\", \"cherry\""
+;; Output: "apple\nbanana\ncherry"
+;;
+;; Input: "'one', 'two', 'three'"
+;; Output: "one\ntwo\nthree"
+;;
+;; We test the NON-INTERACTIVE implementation (cj/--unarrayify) to avoid
+;; mocking region selection. This follows our testing best practice of
+;; separating business logic from UI interaction.
+
+;;; Code:
+
+(require 'ert)
+(require 'testutil-general)
+
+;; Add modules directory to load path
+(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory))
+
+;; Stub dependencies before loading the module
+(defvar cj/custom-keymap (make-sparse-keymap)
+ "Stub keymap for testing.")
+
+;; Now load the actual production module
+(require 'custom-ordering)
+
+;;; Test Helpers
+
+(defun test-unarrayify (input-text)
+ "Test cj/--unarrayify on INPUT-TEXT.
+Returns the transformed string."
+ (with-temp-buffer
+ (insert input-text)
+ (cj/--unarrayify (point-min) (point-max))))
+
+;;; Normal Cases - Double Quotes
+
+(ert-deftest test-unarrayify-double-quotes-simple ()
+ "Should unarrayify double-quoted elements."
+ (let ((result (test-unarrayify "\"apple\", \"banana\", \"cherry\"")))
+ (should (string= result "apple\nbanana\ncherry"))))
+
+(ert-deftest test-unarrayify-double-quotes-single-element ()
+ "Should unarrayify single double-quoted element."
+ (let ((result (test-unarrayify "\"hello\"")))
+ (should (string= result "hello"))))
+
+(ert-deftest test-unarrayify-double-quotes-two-elements ()
+ "Should unarrayify two double-quoted elements."
+ (let ((result (test-unarrayify "\"one\", \"two\"")))
+ (should (string= result "one\ntwo"))))
+
+;;; Normal Cases - Single Quotes
+
+(ert-deftest test-unarrayify-single-quotes-simple ()
+ "Should unarrayify single-quoted elements."
+ (let ((result (test-unarrayify "'alpha', 'beta', 'gamma'")))
+ (should (string= result "alpha\nbeta\ngamma"))))
+
+(ert-deftest test-unarrayify-single-quotes-single-element ()
+ "Should unarrayify single single-quoted element."
+ (let ((result (test-unarrayify "'hello'")))
+ (should (string= result "hello"))))
+
+;;; Normal Cases - Mixed Quotes
+
+(ert-deftest test-unarrayify-mixed-quotes ()
+ "Should unarrayify mixed quote types."
+ (let ((result (test-unarrayify "\"apple\", 'banana', \"cherry\"")))
+ (should (string= result "apple\nbanana\ncherry"))))
+
+;;; Normal Cases - No Quotes
+
+(ert-deftest test-unarrayify-no-quotes ()
+ "Should unarrayify unquoted elements."
+ (let ((result (test-unarrayify "foo, bar, baz")))
+ (should (string= result "foo\nbar\nbaz"))))
+
+;;; Normal Cases - Various Content
+
+(ert-deftest test-unarrayify-with-numbers ()
+ "Should unarrayify numbers."
+ (let ((result (test-unarrayify "\"1\", \"2\", \"3\"")))
+ (should (string= result "1\n2\n3"))))
+
+(ert-deftest test-unarrayify-with-spaces-in-elements ()
+ "Should preserve spaces within elements."
+ (let ((result (test-unarrayify "\"hello world\", \"foo bar\"")))
+ (should (string= result "hello world\nfoo bar"))))
+
+(ert-deftest test-unarrayify-mixed-content ()
+ "Should unarrayify mixed alphanumeric content."
+ (let ((result (test-unarrayify "\"item1\", \"item2\", \"item3\"")))
+ (should (string= result "item1\nitem2\nitem3"))))
+
+;;; Boundary Cases
+
+(ert-deftest test-unarrayify-empty-string ()
+ "Should handle empty string."
+ (let ((result (test-unarrayify "")))
+ (should (string= result ""))))
+
+(ert-deftest test-unarrayify-only-quotes ()
+ "Should remove quotes from quote-only string."
+ (let ((result (test-unarrayify "\"\"")))
+ (should (string= result ""))))
+
+(ert-deftest test-unarrayify-very-long-list ()
+ "Should handle very long list."
+ (let* ((elements (mapcar (lambda (i) (format "\"%d\"" i)) (number-sequence 1 100)))
+ (input (mapconcat #'identity elements ", "))
+ (result (test-unarrayify input))
+ (lines (split-string result "\n")))
+ (should (= 100 (length lines)))))
+
+(ert-deftest test-unarrayify-with-empty-elements ()
+ "Should handle empty quoted elements."
+ (let ((result (test-unarrayify "\"\", \"test\", \"\"")))
+ (should (string= result "\ntest\n"))))
+
+;;; Edge Cases - Nested or Mismatched Quotes
+
+(ert-deftest test-unarrayify-double-quotes-in-single ()
+ "Should handle double quotes inside single-quoted strings."
+ (let ((result (test-unarrayify "'he said \"hello\"', 'world'")))
+ (should (string= result "he said hello\nworld"))))
+
+(ert-deftest test-unarrayify-only-opening-quotes ()
+ "Should remove all quote characters even if mismatched."
+ (let ((result (test-unarrayify "\"apple, \"banana, \"cherry")))
+ (should (string= result "apple\nbanana\ncherry"))))
+
+;;; Error Cases
+
+(ert-deftest test-unarrayify-start-greater-than-end ()
+ "Should error when start > end."
+ (should-error
+ (with-temp-buffer
+ (insert "\"a\", \"b\"")
+ (cj/--unarrayify (point-max) (point-min)))
+ :type 'error))
+
+(ert-deftest test-unarrayify-empty-region ()
+ "Should handle empty region (start == end)."
+ (with-temp-buffer
+ (insert "\"a\", \"b\"")
+ (let ((pos (/ (+ (point-min) (point-max)) 2)))
+ (should (string= "" (cj/--unarrayify pos pos))))))
+
+(provide 'test-custom-ordering-unarrayify)
+;;; test-custom-ordering-unarrayify.el ends here
diff --git a/tests/test-custom-org-agenda-functions.el.disabled b/tests/test-custom-org-agenda-functions.el.disabled
deleted file mode 100644
index 44f9f43d..00000000
--- a/tests/test-custom-org-agenda-functions.el.disabled
+++ /dev/null
@@ -1,94 +0,0 @@
-;;; test-custom-org-agenda-functions.el --- Tests for custom functions in org-agenda -*- lexical-binding: t; -*-
-
-;;; Commentary:
-;; This tests the custom functions created to build the main agenda in org-agenda-config.el
-
-;;; Code:
-
-(add-to-list 'load-path (concat user-emacs-directory "modules"))
-(require 'org-agenda-config)
-
-(ert-deftest test-cj/org-skip-subtree-if-habit-positive ()
- (with-temp-buffer
- (insert "* TODO [#A] Test task\n")
- (insert ":PROPERTIES:\n")
- (insert ":STYLE: habit\n")
- (insert ":RESET_CHECK_BOXES: t\n")
- (insert ":END:\n")
- (org-mode)
- (goto-char (point-min))
- (should (not (eq nil (cj/org-skip-subtree-if-habit))))))
-
-(ert-deftest test-cj/org-skip-subtree-if-habit-negative ()
- (with-temp-buffer
- (insert "* TODO [#A] Test task\n")
- (org-mode)
- (goto-char (point-min))
- (should (eq nil (cj/org-skip-subtree-if-habit)))))
-
-(ert-deftest test-cj/org-skip-subtree-if-priority-positive ()
- (with-temp-buffer
- (insert "* TODO [#A] Test task\n")
- (org-mode)
- (goto-char (point-min))
- (should (not (eq nil (cj/org-skip-subtree-if-priority ?A))))))
-
-(ert-deftest test-cj/org-skip-subtree-if-priority-negative ()
- (erase-buffer)
- (insert "* TODO [#B] Test task\n")
- (org-mode)
- (goto-char (point-min))
- (should (eq nil (cj/org-skip-subtree-if-priority ?A))))
-
-(ert-deftest test-cj/org-skip-subtree-if-priority-boundary0 ()
- (erase-buffer)
- (insert "* TODO Test task\n")
- (org-mode)
- (goto-char (point-min))
- (should (eq nil (cj/org-skip-subtree-if-priority ?A))))
-
-(ert-deftest test-cj/org-skip-subtree-if-priority-boundary1 ()
- (erase-buffer)
- (insert "* Test entry\n")
- (org-mode)
- (goto-char (point-min))
- (should (eq nil (cj/org-skip-subtree-if-priority ?A))))
-
-(ert-deftest test-cj/org-skip-subtree-if-keyword-positive ()
- (with-temp-buffer
- (insert "* TODO [#A] Test task\n")
- (org-mode)
- (goto-char (point-min))
- (should (not (eq nil (cj/org-skip-subtree-if-keyword '("TODO")))))))
-
-(ert-deftest test-cj/org-skip-subtree-if-keyword-positive-multiple ()
- (with-temp-buffer
- (insert "* PROJECT Test entry\n")
- (org-mode)
- (goto-char (point-min))
- (should (not (eq nil (cj/org-skip-subtree-if-keyword '("TODO" "PROJECT")))))))
-
-(ert-deftest test-cj/org-skip-subtree-if-keyword-negative ()
- (erase-buffer)
- (insert "* PROJECT [#A] Test task\n")
- (org-mode)
- (goto-char (point-min))
- (should (eq nil (cj/org-skip-subtree-if-keyword '("TODO")))))
-
-(ert-deftest test-cj/org-skip-subtree-if-keyword-negative-superset ()
- (erase-buffer)
- (insert "* PROJECT [#A] Test task\n")
- (org-mode)
- (goto-char (point-min))
- (should (eq nil (cj/org-skip-subtree-if-keyword '("TODOTODO")))))
-
-(ert-deftest test-cj/org-skip-subtree-if-keyword-negative-multiple ()
- (erase-buffer)
- (insert "* PROJECT [#A] Test task\n")
- (org-mode)
- (goto-char (point-min))
- (should (eq nil (cj/org-skip-subtree-if-keyword '("TODO" "DONE")))))
-
-
-(provide 'test-custom-org-agenda-functions)
-;;; test-custom-org-agenda-functions.el ends here.
diff --git a/tests/test-custom-text-enclose-append.el b/tests/test-custom-text-enclose-append.el
new file mode 100644
index 00000000..3593a7f5
--- /dev/null
+++ b/tests/test-custom-text-enclose-append.el
@@ -0,0 +1,190 @@
+;;; test-custom-text-enclose-append.el --- Tests for cj/--append-to-lines -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; Tests for the cj/--append-to-lines function from custom-text-enclose.el
+;;
+;; This function appends a suffix string to the end of each line in text.
+;; It preserves the structure of lines and handles trailing newlines correctly.
+;;
+;; Examples:
+;; Input: "line1\nline2", suffix: ";"
+;; Output: "line1;\nline2;"
+;;
+;; Input: "single", suffix: "!"
+;; Output: "single!"
+;;
+;; We test the NON-INTERACTIVE implementation (cj/--append-to-lines) to avoid
+;; mocking region selection. This follows our testing best practice of
+;; separating business logic from UI interaction.
+
+;;; Code:
+
+(require 'ert)
+(require 'testutil-general)
+
+;; Add modules directory to load path
+(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory))
+
+;; Stub dependencies before loading the module
+(defvar cj/custom-keymap (make-sparse-keymap)
+ "Stub keymap for testing.")
+
+;; Now load the actual production module
+(require 'custom-text-enclose)
+
+;;; Test Helpers
+
+(defun test-append-to-lines (text suffix)
+ "Test cj/--append-to-lines on TEXT with SUFFIX.
+Returns the transformed string."
+ (cj/--append-to-lines text suffix))
+
+;;; Normal Cases - Single Line
+
+(ert-deftest test-append-single-line ()
+ "Should append to single line."
+ (let ((result (test-append-to-lines "hello" ";")))
+ (should (string= result "hello;"))))
+
+(ert-deftest test-append-single-line-semicolon ()
+ "Should append semicolon to single line."
+ (let ((result (test-append-to-lines "var x = 5" ";")))
+ (should (string= result "var x = 5;"))))
+
+(ert-deftest test-append-single-line-exclamation ()
+ "Should append exclamation mark to single line."
+ (let ((result (test-append-to-lines "Hello world" "!")))
+ (should (string= result "Hello world!"))))
+
+;;; Normal Cases - Multiple Lines
+
+(ert-deftest test-append-two-lines ()
+ "Should append to two lines."
+ (let ((result (test-append-to-lines "line1\nline2" ";")))
+ (should (string= result "line1;\nline2;"))))
+
+(ert-deftest test-append-three-lines ()
+ "Should append to three lines."
+ (let ((result (test-append-to-lines "a\nb\nc" ".")))
+ (should (string= result "a.\nb.\nc."))))
+
+(ert-deftest test-append-many-lines ()
+ "Should append to many lines."
+ (let* ((lines (make-list 10 "line"))
+ (input (mapconcat #'identity lines "\n"))
+ (result (test-append-to-lines input ";"))
+ (result-lines (split-string result "\n")))
+ (should (= 10 (length result-lines)))
+ (should (cl-every (lambda (line) (string-suffix-p ";" line)) result-lines))))
+
+;;; Normal Cases - Various Suffixes
+
+(ert-deftest test-append-comma ()
+ "Should append comma to lines."
+ (let ((result (test-append-to-lines "apple\nbanana" ",")))
+ (should (string= result "apple,\nbanana,"))))
+
+(ert-deftest test-append-multi-char ()
+ "Should append multi-character suffix."
+ (let ((result (test-append-to-lines "line" " // comment")))
+ (should (string= result "line // comment"))))
+
+(ert-deftest test-append-pipe ()
+ "Should append pipe character."
+ (let ((result (test-append-to-lines "col1\ncol2" " |")))
+ (should (string= result "col1 |\ncol2 |"))))
+
+(ert-deftest test-append-empty-suffix ()
+ "Should handle empty suffix."
+ (let ((result (test-append-to-lines "line1\nline2" "")))
+ (should (string= result "line1\nline2"))))
+
+;;; Boundary Cases - Trailing Newlines
+
+(ert-deftest test-append-with-trailing-newline ()
+ "Should preserve trailing newline."
+ (let ((result (test-append-to-lines "line1\nline2\n" ";")))
+ (should (string= result "line1;\nline2;\n"))))
+
+(ert-deftest test-append-no-trailing-newline ()
+ "Should work without trailing newline."
+ (let ((result (test-append-to-lines "line1\nline2" ";")))
+ (should (string= result "line1;\nline2;"))))
+
+(ert-deftest test-append-single-line-with-newline ()
+ "Should preserve trailing newline on single line."
+ (let ((result (test-append-to-lines "line\n" ";")))
+ (should (string= result "line;\n"))))
+
+;;; Boundary Cases - Empty Lines
+
+(ert-deftest test-append-empty-line-between ()
+ "Should append to empty line between other lines."
+ (let ((result (test-append-to-lines "line1\n\nline3" ";")))
+ (should (string= result "line1;\n;\nline3;"))))
+
+(ert-deftest test-append-only-empty-lines ()
+ "Should append to only empty lines."
+ (let ((result (test-append-to-lines "\n\n" ";")))
+ (should (string= result ";\n;\n"))))
+
+(ert-deftest test-append-empty-first-line ()
+ "Should append to empty first line."
+ (let ((result (test-append-to-lines "\nline2\nline3" ";")))
+ (should (string= result ";\nline2;\nline3;"))))
+
+;;; Boundary Cases - Whitespace
+
+(ert-deftest test-append-preserves-leading-whitespace ()
+ "Should preserve leading whitespace."
+ (let ((result (test-append-to-lines " line1\n line2" ";")))
+ (should (string= result " line1;\n line2;"))))
+
+(ert-deftest test-append-preserves-trailing-whitespace ()
+ "Should preserve trailing whitespace on line."
+ (let ((result (test-append-to-lines "line1 \nline2 " ";")))
+ (should (string= result "line1 ;\nline2 ;"))))
+
+(ert-deftest test-append-whitespace-only-line ()
+ "Should append to whitespace-only line."
+ (let ((result (test-append-to-lines "line1\n \nline3" ";")))
+ (should (string= result "line1;\n ;\nline3;"))))
+
+;;; Boundary Cases - Special Cases
+
+(ert-deftest test-append-empty-string ()
+ "Should handle empty string."
+ (let ((result (test-append-to-lines "" ";")))
+ (should (string= result ";"))))
+
+(ert-deftest test-append-very-long-line ()
+ "Should append to very long line."
+ (let* ((long-line (make-string 1000 ?a))
+ (result (test-append-to-lines long-line ";")))
+ (should (string-suffix-p ";" result))
+ (should (= (length result) 1001))))
+
+(ert-deftest test-append-with-existing-suffix ()
+ "Should append even if line already has the suffix."
+ (let ((result (test-append-to-lines "line;" ";")))
+ (should (string= result "line;;"))))
+
+;;; Edge Cases - Special Characters in Suffix
+
+(ert-deftest test-append-newline-suffix ()
+ "Should append newline as suffix."
+ (let ((result (test-append-to-lines "line1\nline2" "\n")))
+ (should (string= result "line1\n\nline2\n"))))
+
+(ert-deftest test-append-tab-suffix ()
+ "Should append tab as suffix."
+ (let ((result (test-append-to-lines "col1\ncol2" "\t")))
+ (should (string= result "col1\t\ncol2\t"))))
+
+(ert-deftest test-append-quote-suffix ()
+ "Should append quote as suffix."
+ (let ((result (test-append-to-lines "value1\nvalue2" "\"")))
+ (should (string= result "value1\"\nvalue2\""))))
+
+(provide 'test-custom-text-enclose-append)
+;;; test-custom-text-enclose-append.el ends here
diff --git a/tests/test-custom-text-enclose-indent.el b/tests/test-custom-text-enclose-indent.el
new file mode 100644
index 00000000..e9042d35
--- /dev/null
+++ b/tests/test-custom-text-enclose-indent.el
@@ -0,0 +1,241 @@
+;;; test-custom-text-enclose-indent.el --- Tests for cj/--indent-lines and cj/--dedent-lines -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; Tests for the cj/--indent-lines and cj/--dedent-lines functions from custom-text-enclose.el
+;;
+;; cj/--indent-lines adds leading whitespace (spaces or tabs) to each line.
+;; cj/--dedent-lines removes up to COUNT leading whitespace characters from each line.
+;;
+;; Examples (indent):
+;; Input: "line1\nline2", count: 4, use-tabs: nil
+;; Output: " line1\n line2"
+;;
+;; Examples (dedent):
+;; Input: " line1\n line2", count: 4
+;; Output: "line1\nline2"
+;;
+;; We test the NON-INTERACTIVE implementations to avoid mocking user input.
+
+;;; Code:
+
+(require 'ert)
+(require 'testutil-general)
+
+;; Add modules directory to load path
+(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory))
+
+;; Stub dependencies before loading the module
+(defvar cj/custom-keymap (make-sparse-keymap)
+ "Stub keymap for testing.")
+
+;; Now load the actual production module
+(require 'custom-text-enclose)
+
+;;; Test Helpers
+
+(defun test-indent (text count use-tabs)
+ "Test cj/--indent-lines on TEXT with COUNT and USE-TABS.
+Returns the transformed string."
+ (cj/--indent-lines text count use-tabs))
+
+(defun test-dedent (text count)
+ "Test cj/--dedent-lines on TEXT with COUNT.
+Returns the transformed string."
+ (cj/--dedent-lines text count))
+
+;;; Indent Tests - Normal Cases with Spaces
+
+(ert-deftest test-indent-single-line-4-spaces ()
+ "Should indent single line with 4 spaces."
+ (let ((result (test-indent "line" 4 nil)))
+ (should (string= result " line"))))
+
+(ert-deftest test-indent-two-lines-4-spaces ()
+ "Should indent two lines with 4 spaces."
+ (let ((result (test-indent "line1\nline2" 4 nil)))
+ (should (string= result " line1\n line2"))))
+
+(ert-deftest test-indent-three-lines-2-spaces ()
+ "Should indent three lines with 2 spaces."
+ (let ((result (test-indent "a\nb\nc" 2 nil)))
+ (should (string= result " a\n b\n c"))))
+
+(ert-deftest test-indent-many-lines ()
+ "Should indent many lines."
+ (let ((result (test-indent "1\n2\n3\n4\n5" 4 nil)))
+ (should (string= result " 1\n 2\n 3\n 4\n 5"))))
+
+;;; Indent Tests - Normal Cases with Tabs
+
+(ert-deftest test-indent-single-line-1-tab ()
+ "Should indent single line with 1 tab."
+ (let ((result (test-indent "line" 1 t)))
+ (should (string= result "\tline"))))
+
+(ert-deftest test-indent-two-lines-1-tab ()
+ "Should indent two lines with 1 tab."
+ (let ((result (test-indent "line1\nline2" 1 t)))
+ (should (string= result "\tline1\n\tline2"))))
+
+(ert-deftest test-indent-with-2-tabs ()
+ "Should indent with 2 tabs."
+ (let ((result (test-indent "code" 2 t)))
+ (should (string= result "\t\tcode"))))
+
+;;; Indent Tests - Boundary Cases
+
+(ert-deftest test-indent-empty-string ()
+ "Should indent empty string."
+ (let ((result (test-indent "" 4 nil)))
+ (should (string= result " "))))
+
+(ert-deftest test-indent-zero-count ()
+ "Should not indent with count 0."
+ (let ((result (test-indent "line" 0 nil)))
+ (should (string= result "line"))))
+
+(ert-deftest test-indent-already-indented ()
+ "Should add more indentation to already indented lines."
+ (let ((result (test-indent " line1\n line2" 2 nil)))
+ (should (string= result " line1\n line2"))))
+
+(ert-deftest test-indent-empty-lines ()
+ "Should indent empty lines."
+ (let ((result (test-indent "line1\n\nline3" 4 nil)))
+ (should (string= result " line1\n \n line3"))))
+
+(ert-deftest test-indent-trailing-newline ()
+ "Should preserve trailing newline."
+ (let ((result (test-indent "line1\nline2\n" 4 nil)))
+ (should (string= result " line1\n line2\n"))))
+
+(ert-deftest test-indent-no-trailing-newline ()
+ "Should work without trailing newline."
+ (let ((result (test-indent "line1\nline2" 4 nil)))
+ (should (string= result " line1\n line2"))))
+
+;;; Dedent Tests - Normal Cases
+
+(ert-deftest test-dedent-single-line-4-spaces ()
+ "Should dedent single line with 4 spaces."
+ (let ((result (test-dedent " line" 4)))
+ (should (string= result "line"))))
+
+(ert-deftest test-dedent-two-lines-4-spaces ()
+ "Should dedent two lines with 4 spaces."
+ (let ((result (test-dedent " line1\n line2" 4)))
+ (should (string= result "line1\nline2"))))
+
+(ert-deftest test-dedent-three-lines-2-spaces ()
+ "Should dedent three lines with 2 spaces."
+ (let ((result (test-dedent " a\n b\n c" 2)))
+ (should (string= result "a\nb\nc"))))
+
+(ert-deftest test-dedent-with-tabs ()
+ "Should dedent lines with tabs."
+ (let ((result (test-dedent "\tline1\n\tline2" 1)))
+ (should (string= result "line1\nline2"))))
+
+(ert-deftest test-dedent-mixed-spaces-tabs ()
+ "Should dedent mixed spaces and tabs."
+ (let ((result (test-dedent " \tline" 3)))
+ (should (string= result "line"))))
+
+;;; Dedent Tests - Partial Dedent
+
+(ert-deftest test-dedent-partial ()
+ "Should dedent only COUNT characters."
+ (let ((result (test-dedent " line" 2)))
+ (should (string= result " line"))))
+
+(ert-deftest test-dedent-less-than-count ()
+ "Should dedent all available spaces when less than COUNT."
+ (let ((result (test-dedent " line" 4)))
+ (should (string= result "line"))))
+
+(ert-deftest test-dedent-no-leading-space ()
+ "Should not affect lines with no leading whitespace."
+ (let ((result (test-dedent "line" 4)))
+ (should (string= result "line"))))
+
+(ert-deftest test-dedent-varying-indentation ()
+ "Should dedent each line independently."
+ (let ((result (test-dedent " line1\n line2\nline3" 2)))
+ (should (string= result " line1\nline2\nline3"))))
+
+;;; Dedent Tests - Boundary Cases
+
+(ert-deftest test-dedent-empty-string ()
+ "Should handle empty string."
+ (let ((result (test-dedent "" 4)))
+ (should (string= result ""))))
+
+(ert-deftest test-dedent-zero-count ()
+ "Should not dedent with count 0."
+ (let ((result (test-dedent " line" 0)))
+ (should (string= result " line"))))
+
+(ert-deftest test-dedent-empty-lines ()
+ "Should handle empty lines."
+ (let ((result (test-dedent " line1\n \n line3" 4)))
+ (should (string= result "line1\n\nline3"))))
+
+(ert-deftest test-dedent-only-whitespace ()
+ "Should dedent whitespace-only lines."
+ (let ((result (test-dedent " " 4)))
+ (should (string= result ""))))
+
+(ert-deftest test-dedent-trailing-newline ()
+ "Should preserve trailing newline."
+ (let ((result (test-dedent " line1\n line2\n" 4)))
+ (should (string= result "line1\nline2\n"))))
+
+(ert-deftest test-dedent-preserves-internal-spaces ()
+ "Should not affect internal whitespace."
+ (let ((result (test-dedent " hello world" 4)))
+ (should (string= result "hello world"))))
+
+;;; Round-trip Tests
+
+(ert-deftest test-indent-dedent-roundtrip ()
+ "Should be able to indent then dedent back to original."
+ (let* ((original "line1\nline2")
+ (indented (test-indent original 4 nil))
+ (dedented (test-dedent indented 4)))
+ (should (string= dedented original))))
+
+(ert-deftest test-dedent-indent-roundtrip ()
+ "Should be able to dedent then indent back to original."
+ (let* ((original " line1\n line2")
+ (dedented (test-dedent original 4))
+ (indented (test-indent dedented 4 nil)))
+ (should (string= indented original))))
+
+;;; Edge Cases
+
+(ert-deftest test-indent-very-long-line ()
+ "Should indent very long line."
+ (let* ((long-line (make-string 1000 ?a))
+ (result (test-indent long-line 4 nil)))
+ (should (string-prefix-p " " result))
+ (should (= (length result) 1004))))
+
+(ert-deftest test-dedent-very-indented ()
+ "Should dedent very indented line."
+ (let* ((many-spaces (make-string 100 ?\s))
+ (text (concat many-spaces "text"))
+ (result (test-dedent text 50)))
+ (should (string-prefix-p (make-string 50 ?\s) result))))
+
+(ert-deftest test-indent-with-existing-tabs ()
+ "Should indent lines that already have tabs."
+ (let ((result (test-indent "\tcode" 4 nil)))
+ (should (string= result " \tcode"))))
+
+(ert-deftest test-dedent-stops-at-non-whitespace ()
+ "Should stop dedenting at first non-whitespace character."
+ (let ((result (test-dedent " a b" 4)))
+ (should (string= result "a b"))))
+
+(provide 'test-custom-text-enclose-indent)
+;;; test-custom-text-enclose-indent.el ends here
diff --git a/tests/test-custom-text-enclose-prepend.el b/tests/test-custom-text-enclose-prepend.el
new file mode 100644
index 00000000..e03375ff
--- /dev/null
+++ b/tests/test-custom-text-enclose-prepend.el
@@ -0,0 +1,207 @@
+;;; test-custom-text-enclose-prepend.el --- Tests for cj/--prepend-to-lines -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; Tests for the cj/--prepend-to-lines function from custom-text-enclose.el
+;;
+;; This function prepends a prefix string to the beginning of each line in text.
+;; It preserves the structure of lines and handles trailing newlines correctly.
+;;
+;; Examples:
+;; Input: "line1\nline2", prefix: "// "
+;; Output: "// line1\n// line2"
+;;
+;; Input: "single", prefix: "> "
+;; Output: "> single"
+;;
+;; We test the NON-INTERACTIVE implementation (cj/--prepend-to-lines) to avoid
+;; mocking region selection. This follows our testing best practice of
+;; separating business logic from UI interaction.
+
+;;; Code:
+
+(require 'ert)
+(require 'testutil-general)
+
+;; Add modules directory to load path
+(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory))
+
+;; Stub dependencies before loading the module
+(defvar cj/custom-keymap (make-sparse-keymap)
+ "Stub keymap for testing.")
+
+;; Now load the actual production module
+(require 'custom-text-enclose)
+
+;;; Test Helpers
+
+(defun test-prepend-to-lines (text prefix)
+ "Test cj/--prepend-to-lines on TEXT with PREFIX.
+Returns the transformed string."
+ (cj/--prepend-to-lines text prefix))
+
+;;; Normal Cases - Single Line
+
+(ert-deftest test-prepend-single-line ()
+ "Should prepend to single line."
+ (let ((result (test-prepend-to-lines "hello" "> ")))
+ (should (string= result "> hello"))))
+
+(ert-deftest test-prepend-single-line-comment ()
+ "Should prepend comment marker to single line."
+ (let ((result (test-prepend-to-lines "code here" "// ")))
+ (should (string= result "// code here"))))
+
+(ert-deftest test-prepend-single-line-bullet ()
+ "Should prepend bullet to single line."
+ (let ((result (test-prepend-to-lines "item" "- ")))
+ (should (string= result "- item"))))
+
+;;; Normal Cases - Multiple Lines
+
+(ert-deftest test-prepend-two-lines ()
+ "Should prepend to two lines."
+ (let ((result (test-prepend-to-lines "line1\nline2" "> ")))
+ (should (string= result "> line1\n> line2"))))
+
+(ert-deftest test-prepend-three-lines ()
+ "Should prepend to three lines."
+ (let ((result (test-prepend-to-lines "a\nb\nc" "* ")))
+ (should (string= result "* a\n* b\n* c"))))
+
+(ert-deftest test-prepend-many-lines ()
+ "Should prepend to many lines."
+ (let* ((lines (make-list 10 "line"))
+ (input (mapconcat #'identity lines "\n"))
+ (result (test-prepend-to-lines input "# "))
+ (result-lines (split-string result "\n")))
+ (should (= 10 (length result-lines)))
+ (should (cl-every (lambda (line) (string-prefix-p "# " line)) result-lines))))
+
+;;; Normal Cases - Various Prefixes
+
+(ert-deftest test-prepend-comment-marker ()
+ "Should prepend comment marker."
+ (let ((result (test-prepend-to-lines "line1\nline2" "// ")))
+ (should (string= result "// line1\n// line2"))))
+
+(ert-deftest test-prepend-hash-comment ()
+ "Should prepend hash comment."
+ (let ((result (test-prepend-to-lines "line1\nline2" "# ")))
+ (should (string= result "# line1\n# line2"))))
+
+(ert-deftest test-prepend-multi-char ()
+ "Should prepend multi-character prefix."
+ (let ((result (test-prepend-to-lines "line" "TODO: ")))
+ (should (string= result "TODO: line"))))
+
+(ert-deftest test-prepend-empty-prefix ()
+ "Should handle empty prefix."
+ (let ((result (test-prepend-to-lines "line1\nline2" "")))
+ (should (string= result "line1\nline2"))))
+
+;;; Boundary Cases - Trailing Newlines
+
+(ert-deftest test-prepend-with-trailing-newline ()
+ "Should preserve trailing newline."
+ (let ((result (test-prepend-to-lines "line1\nline2\n" "> ")))
+ (should (string= result "> line1\n> line2\n"))))
+
+(ert-deftest test-prepend-no-trailing-newline ()
+ "Should work without trailing newline."
+ (let ((result (test-prepend-to-lines "line1\nline2" "> ")))
+ (should (string= result "> line1\n> line2"))))
+
+(ert-deftest test-prepend-single-line-with-newline ()
+ "Should preserve trailing newline on single line."
+ (let ((result (test-prepend-to-lines "line\n" "> ")))
+ (should (string= result "> line\n"))))
+
+;;; Boundary Cases - Empty Lines
+
+(ert-deftest test-prepend-empty-line-between ()
+ "Should prepend to empty line between other lines."
+ (let ((result (test-prepend-to-lines "line1\n\nline3" "> ")))
+ (should (string= result "> line1\n> \n> line3"))))
+
+(ert-deftest test-prepend-only-empty-lines ()
+ "Should prepend to only empty lines."
+ (let ((result (test-prepend-to-lines "\n\n" "> ")))
+ (should (string= result "> \n> \n"))))
+
+(ert-deftest test-prepend-empty-first-line ()
+ "Should prepend to empty first line."
+ (let ((result (test-prepend-to-lines "\nline2\nline3" "> ")))
+ (should (string= result "> \n> line2\n> line3"))))
+
+;;; Boundary Cases - Whitespace
+
+(ert-deftest test-prepend-preserves-leading-whitespace ()
+ "Should preserve leading whitespace after prefix."
+ (let ((result (test-prepend-to-lines " line1\n line2" "// ")))
+ (should (string= result "// line1\n// line2"))))
+
+(ert-deftest test-prepend-preserves-trailing-whitespace ()
+ "Should preserve trailing whitespace on line."
+ (let ((result (test-prepend-to-lines "line1 \nline2 " "> ")))
+ (should (string= result "> line1 \n> line2 "))))
+
+(ert-deftest test-prepend-whitespace-only-line ()
+ "Should prepend to whitespace-only line."
+ (let ((result (test-prepend-to-lines "line1\n \nline3" "> ")))
+ (should (string= result "> line1\n> \n> line3"))))
+
+;;; Boundary Cases - Special Cases
+
+(ert-deftest test-prepend-empty-string ()
+ "Should handle empty string."
+ (let ((result (test-prepend-to-lines "" "> ")))
+ (should (string= result "> "))))
+
+(ert-deftest test-prepend-very-long-line ()
+ "Should prepend to very long line."
+ (let* ((long-line (make-string 1000 ?a))
+ (result (test-prepend-to-lines long-line "> ")))
+ (should (string-prefix-p "> " result))
+ (should (= (length result) 1002))))
+
+(ert-deftest test-prepend-with-existing-prefix ()
+ "Should prepend even if line already has the prefix."
+ (let ((result (test-prepend-to-lines "> line" "> ")))
+ (should (string= result "> > line"))))
+
+;;; Edge Cases - Special Characters in Prefix
+
+(ert-deftest test-prepend-newline-prefix ()
+ "Should prepend newline as prefix."
+ (let ((result (test-prepend-to-lines "line1\nline2" "\n")))
+ (should (string= result "\nline1\n\nline2"))))
+
+(ert-deftest test-prepend-tab-prefix ()
+ "Should prepend tab as prefix."
+ (let ((result (test-prepend-to-lines "line1\nline2" "\t")))
+ (should (string= result "\tline1\n\tline2"))))
+
+(ert-deftest test-prepend-quote-prefix ()
+ "Should prepend quote as prefix."
+ (let ((result (test-prepend-to-lines "line1\nline2" "\"")))
+ (should (string= result "\"line1\n\"line2"))))
+
+;;; Edge Cases - Common Use Cases
+
+(ert-deftest test-prepend-markdown-quote ()
+ "Should prepend markdown quote marker."
+ (let ((result (test-prepend-to-lines "quote text\nmore text" "> ")))
+ (should (string= result "> quote text\n> more text"))))
+
+(ert-deftest test-prepend-numbered-list ()
+ "Should prepend numbers (though simpler uses would vary the prefix)."
+ (let ((result (test-prepend-to-lines "item" "1. ")))
+ (should (string= result "1. item"))))
+
+(ert-deftest test-prepend-indentation ()
+ "Should prepend indentation spaces."
+ (let ((result (test-prepend-to-lines "code\nmore" " ")))
+ (should (string= result " code\n more"))))
+
+(provide 'test-custom-text-enclose-prepend)
+;;; test-custom-text-enclose-prepend.el ends here
diff --git a/tests/test-custom-text-enclose-surround.el b/tests/test-custom-text-enclose-surround.el
new file mode 100644
index 00000000..dfed20a7
--- /dev/null
+++ b/tests/test-custom-text-enclose-surround.el
@@ -0,0 +1,200 @@
+;;; test-custom-text-enclose-surround.el --- Tests for cj/--surround -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; Tests for the cj/--surround function from custom-text-enclose.el
+;;
+;; This function surrounds text with a given string.
+;; The surround string is both prepended and appended to the text.
+;;
+;; Examples:
+;; Input: "hello", surround: "\""
+;; Output: "\"hello\""
+;;
+;; Input: "world", surround: "**"
+;; Output: "**world**"
+;;
+;; We test the NON-INTERACTIVE implementation (cj/--surround) to avoid
+;; mocking user input. This follows our testing best practice of
+;; separating business logic from UI interaction.
+
+;;; Code:
+
+(require 'ert)
+(require 'testutil-general)
+
+;; Add modules directory to load path
+(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory))
+
+;; Stub dependencies before loading the module
+(defvar cj/custom-keymap (make-sparse-keymap)
+ "Stub keymap for testing.")
+
+;; Now load the actual production module
+(require 'custom-text-enclose)
+
+;;; Test Helpers
+
+(defun test-surround (text surround-string)
+ "Test cj/--surround on TEXT with SURROUND-STRING.
+Returns the transformed string."
+ (cj/--surround text surround-string))
+
+;;; Normal Cases - Common Surround Strings
+
+(ert-deftest test-surround-double-quotes ()
+ "Should surround text with double quotes."
+ (let ((result (test-surround "hello" "\"")))
+ (should (string= result "\"hello\""))))
+
+(ert-deftest test-surround-single-quotes ()
+ "Should surround text with single quotes."
+ (let ((result (test-surround "world" "'")))
+ (should (string= result "'world'"))))
+
+(ert-deftest test-surround-parentheses ()
+ "Should surround text with parentheses."
+ (let ((result (test-surround "text" "(")))
+ (should (string= result "(text("))))
+
+(ert-deftest test-surround-square-brackets ()
+ "Should surround text with square brackets."
+ (let ((result (test-surround "item" "[")))
+ (should (string= result "[item["))))
+
+(ert-deftest test-surround-asterisks ()
+ "Should surround text with asterisks for markdown."
+ (let ((result (test-surround "bold" "*")))
+ (should (string= result "*bold*"))))
+
+(ert-deftest test-surround-double-asterisks ()
+ "Should surround text with double asterisks."
+ (let ((result (test-surround "bold" "**")))
+ (should (string= result "**bold**"))))
+
+;;; Normal Cases - Multi-Character Surround Strings
+
+(ert-deftest test-surround-html-tag ()
+ "Should surround text with HTML-like tags."
+ (let ((result (test-surround "content" "<tag>")))
+ (should (string= result "<tag>content<tag>"))))
+
+(ert-deftest test-surround-backticks ()
+ "Should surround text with backticks for code."
+ (let ((result (test-surround "code" "`")))
+ (should (string= result "`code`"))))
+
+(ert-deftest test-surround-triple-backticks ()
+ "Should surround text with triple backticks."
+ (let ((result (test-surround "code block" "```")))
+ (should (string= result "```code block```"))))
+
+(ert-deftest test-surround-custom-delimiter ()
+ "Should surround text with custom delimiter."
+ (let ((result (test-surround "data" "||")))
+ (should (string= result "||data||"))))
+
+;;; Normal Cases - Various Text Content
+
+(ert-deftest test-surround-single-word ()
+ "Should surround single word."
+ (let ((result (test-surround "word" "\"")))
+ (should (string= result "\"word\""))))
+
+(ert-deftest test-surround-multiple-words ()
+ "Should surround multiple words."
+ (let ((result (test-surround "hello world" "\"")))
+ (should (string= result "\"hello world\""))))
+
+(ert-deftest test-surround-sentence ()
+ "Should surround full sentence."
+ (let ((result (test-surround "This is a sentence." "\"")))
+ (should (string= result "\"This is a sentence.\""))))
+
+(ert-deftest test-surround-with-numbers ()
+ "Should surround text with numbers."
+ (let ((result (test-surround "123" "'")))
+ (should (string= result "'123'"))))
+
+(ert-deftest test-surround-with-special-chars ()
+ "Should surround text with special characters."
+ (let ((result (test-surround "hello@world.com" "\"")))
+ (should (string= result "\"hello@world.com\""))))
+
+;;; Normal Cases - Multiline Text
+
+(ert-deftest test-surround-multiline ()
+ "Should surround multiline text."
+ (let ((result (test-surround "line1\nline2\nline3" "\"")))
+ (should (string= result "\"line1\nline2\nline3\""))))
+
+(ert-deftest test-surround-text-with-newlines ()
+ "Should surround text containing newlines."
+ (let ((result (test-surround "first\nsecond" "**")))
+ (should (string= result "**first\nsecond**"))))
+
+;;; Boundary Cases
+
+(ert-deftest test-surround-empty-string ()
+ "Should surround empty string."
+ (let ((result (test-surround "" "\"")))
+ (should (string= result "\"\""))))
+
+(ert-deftest test-surround-single-character ()
+ "Should surround single character."
+ (let ((result (test-surround "x" "\"")))
+ (should (string= result "\"x\""))))
+
+(ert-deftest test-surround-empty-surround-string ()
+ "Should handle empty surround string."
+ (let ((result (test-surround "hello" "")))
+ (should (string= result "hello"))))
+
+(ert-deftest test-surround-very-long-text ()
+ "Should surround very long text."
+ (let* ((long-text (make-string 1000 ?a))
+ (result (test-surround long-text "\"")))
+ (should (string-prefix-p "\"" result))
+ (should (string-suffix-p "\"" result))
+ (should (= (length result) 1002))))
+
+(ert-deftest test-surround-whitespace-only ()
+ "Should surround whitespace-only text."
+ (let ((result (test-surround " " "\"")))
+ (should (string= result "\" \""))))
+
+(ert-deftest test-surround-tabs ()
+ "Should surround text with tabs."
+ (let ((result (test-surround "\t\ttext\t\t" "\"")))
+ (should (string= result "\"\t\ttext\t\t\""))))
+
+;;; Edge Cases - Already Surrounded
+
+(ert-deftest test-surround-already-quoted ()
+ "Should surround text that is already quoted."
+ (let ((result (test-surround "\"hello\"" "\"")))
+ (should (string= result "\"\"hello\"\""))))
+
+(ert-deftest test-surround-nested ()
+ "Should surround text creating nested delimiters."
+ (let ((result (test-surround "'inner'" "\"")))
+ (should (string= result "\"'inner'\""))))
+
+;;; Edge Cases - Special Surround Strings
+
+(ert-deftest test-surround-space ()
+ "Should surround text with spaces."
+ (let ((result (test-surround "text" " ")))
+ (should (string= result " text "))))
+
+(ert-deftest test-surround-newline ()
+ "Should surround text with newlines."
+ (let ((result (test-surround "text" "\n")))
+ (should (string= result "\ntext\n"))))
+
+(ert-deftest test-surround-mixed-delimiters ()
+ "Should surround with mixed delimiter string."
+ (let ((result (test-surround "content" "<>")))
+ (should (string= result "<>content<>"))))
+
+(provide 'test-custom-text-enclose-surround)
+;;; test-custom-text-enclose-surround.el ends here
diff --git a/tests/test-custom-text-enclose-unwrap.el b/tests/test-custom-text-enclose-unwrap.el
new file mode 100644
index 00000000..a308b644
--- /dev/null
+++ b/tests/test-custom-text-enclose-unwrap.el
@@ -0,0 +1,266 @@
+;;; test-custom-text-enclose-unwrap.el --- Tests for cj/--unwrap -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; Tests for the cj/--unwrap function from custom-text-enclose.el
+;;
+;; This function removes surrounding delimiters from text.
+;; It checks if text starts with opening and ends with closing,
+;; and if so, removes them.
+;;
+;; Examples:
+;; Input: "(text)", opening: "(", closing: ")"
+;; Output: "text"
+;;
+;; Input: "<div>content</div>", opening: "<div>", closing: "</div>"
+;; Output: "content"
+;;
+;; We test the NON-INTERACTIVE implementation (cj/--unwrap) to avoid
+;; mocking user input. This follows our testing best practice of
+;; separating business logic from UI interaction.
+
+;;; Code:
+
+(require 'ert)
+(require 'testutil-general)
+
+;; Add modules directory to load path
+(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory))
+
+;; Stub dependencies before loading the module
+(defvar cj/custom-keymap (make-sparse-keymap)
+ "Stub keymap for testing.")
+
+;; Now load the actual production module
+(require 'custom-text-enclose)
+
+;;; Test Helpers
+
+(defun test-unwrap (text opening closing)
+ "Test cj/--unwrap on TEXT with OPENING and CLOSING.
+Returns the transformed string."
+ (cj/--unwrap text opening closing))
+
+;;; Normal Cases - Common Bracket Types
+
+(ert-deftest test-unwrap-parentheses ()
+ "Should unwrap text with parentheses."
+ (let ((result (test-unwrap "(text)" "(" ")")))
+ (should (string= result "text"))))
+
+(ert-deftest test-unwrap-square-brackets ()
+ "Should unwrap text with square brackets."
+ (let ((result (test-unwrap "[item]" "[" "]")))
+ (should (string= result "item"))))
+
+(ert-deftest test-unwrap-curly-braces ()
+ "Should unwrap text with curly braces."
+ (let ((result (test-unwrap "{code}" "{" "}")))
+ (should (string= result "code"))))
+
+(ert-deftest test-unwrap-angle-brackets ()
+ "Should unwrap text with angle brackets."
+ (let ((result (test-unwrap "<tag>" "<" ">")))
+ (should (string= result "tag"))))
+
+;;; Normal Cases - HTML/XML Tags
+
+(ert-deftest test-unwrap-html-div ()
+ "Should unwrap HTML div tags."
+ (let ((result (test-unwrap "<div>content</div>" "<div>" "</div>")))
+ (should (string= result "content"))))
+
+(ert-deftest test-unwrap-html-span ()
+ "Should unwrap HTML span tags."
+ (let ((result (test-unwrap "<span>text</span>" "<span>" "</span>")))
+ (should (string= result "text"))))
+
+(ert-deftest test-unwrap-xml-tag ()
+ "Should unwrap XML tags."
+ (let ((result (test-unwrap "<item>data</item>" "<item>" "</item>")))
+ (should (string= result "data"))))
+
+(ert-deftest test-unwrap-html-with-attributes ()
+ "Should unwrap HTML tag containing attributes."
+ (let ((result (test-unwrap "<a href=\"url\">link</a>" "<a href=\"url\">" "</a>")))
+ (should (string= result "link"))))
+
+;;; Normal Cases - Markdown Syntax
+
+(ert-deftest test-unwrap-markdown-bold ()
+ "Should unwrap markdown bold syntax."
+ (let ((result (test-unwrap "**bold**" "**" "**")))
+ (should (string= result "bold"))))
+
+(ert-deftest test-unwrap-markdown-italic ()
+ "Should unwrap markdown italic syntax."
+ (let ((result (test-unwrap "*italic*" "*" "*")))
+ (should (string= result "italic"))))
+
+(ert-deftest test-unwrap-markdown-code ()
+ "Should unwrap markdown code syntax."
+ (let ((result (test-unwrap "`code`" "`" "`")))
+ (should (string= result "code"))))
+
+(ert-deftest test-unwrap-quotes ()
+ "Should unwrap double quotes."
+ (let ((result (test-unwrap "\"text\"" "\"" "\"")))
+ (should (string= result "text"))))
+
+;;; Normal Cases - Various Content
+
+(ert-deftest test-unwrap-single-word ()
+ "Should unwrap single word."
+ (let ((result (test-unwrap "(word)" "(" ")")))
+ (should (string= result "word"))))
+
+(ert-deftest test-unwrap-multiple-words ()
+ "Should unwrap multiple words."
+ (let ((result (test-unwrap "(hello world)" "(" ")")))
+ (should (string= result "hello world"))))
+
+(ert-deftest test-unwrap-sentence ()
+ "Should unwrap full sentence."
+ (let ((result (test-unwrap "(This is a sentence.)" "(" ")")))
+ (should (string= result "This is a sentence."))))
+
+(ert-deftest test-unwrap-with-numbers ()
+ "Should unwrap text with numbers."
+ (let ((result (test-unwrap "[123]" "[" "]")))
+ (should (string= result "123"))))
+
+(ert-deftest test-unwrap-with-special-chars ()
+ "Should unwrap text with special characters."
+ (let ((result (test-unwrap "<hello@world.com>" "<" ">")))
+ (should (string= result "hello@world.com"))))
+
+;;; Normal Cases - Multiline Text
+
+(ert-deftest test-unwrap-multiline ()
+ "Should unwrap multiline text."
+ (let ((result (test-unwrap "<div>line1\nline2\nline3</div>" "<div>" "</div>")))
+ (should (string= result "line1\nline2\nline3"))))
+
+(ert-deftest test-unwrap-text-with-newlines ()
+ "Should unwrap text containing newlines."
+ (let ((result (test-unwrap "(first\nsecond)" "(" ")")))
+ (should (string= result "first\nsecond"))))
+
+;;; Boundary Cases - No Match
+
+(ert-deftest test-unwrap-no-opening ()
+ "Should not unwrap when opening is missing."
+ (let ((result (test-unwrap "text)" "(" ")")))
+ (should (string= result "text)"))))
+
+(ert-deftest test-unwrap-no-closing ()
+ "Should not unwrap when closing is missing."
+ (let ((result (test-unwrap "(text" "(" ")")))
+ (should (string= result "(text"))))
+
+(ert-deftest test-unwrap-neither-delimiter ()
+ "Should not unwrap when neither delimiter is present."
+ (let ((result (test-unwrap "text" "(" ")")))
+ (should (string= result "text"))))
+
+(ert-deftest test-unwrap-wrong-opening ()
+ "Should not unwrap with wrong opening delimiter."
+ (let ((result (test-unwrap "[text)" "(" ")")))
+ (should (string= result "[text)"))))
+
+(ert-deftest test-unwrap-wrong-closing ()
+ "Should not unwrap with wrong closing delimiter."
+ (let ((result (test-unwrap "(text]" "(" ")")))
+ (should (string= result "(text]"))))
+
+;;; Boundary Cases - Empty
+
+(ert-deftest test-unwrap-empty-content ()
+ "Should unwrap to empty string."
+ (let ((result (test-unwrap "()" "(" ")")))
+ (should (string= result ""))))
+
+(ert-deftest test-unwrap-just-delimiters ()
+ "Should unwrap when only delimiters present."
+ (let ((result (test-unwrap "[]" "[" "]")))
+ (should (string= result ""))))
+
+(ert-deftest test-unwrap-empty-string ()
+ "Should return empty string unchanged."
+ (let ((result (test-unwrap "" "(" ")")))
+ (should (string= result ""))))
+
+(ert-deftest test-unwrap-too-short ()
+ "Should not unwrap when text is shorter than delimiters."
+ (let ((result (test-unwrap "x" "<div>" "</div>")))
+ (should (string= result "x"))))
+
+;;; Boundary Cases - Nested/Multiple
+
+(ert-deftest test-unwrap-nested-same ()
+ "Should unwrap only outer layer of nested delimiters."
+ (let ((result (test-unwrap "((text))" "(" ")")))
+ (should (string= result "(text)"))))
+
+(ert-deftest test-unwrap-nested-different ()
+ "Should unwrap outer layer with different inner delimiters."
+ (let ((result (test-unwrap "([text])" "(" ")")))
+ (should (string= result "[text]"))))
+
+(ert-deftest test-unwrap-multiple-in-content ()
+ "Should not unwrap when delimiters appear in content."
+ (let ((result (test-unwrap "(a)b(c)" "(" ")")))
+ (should (string= result "a)b(c"))))
+
+;;; Edge Cases - Special Delimiters
+
+(ert-deftest test-unwrap-asymmetric-length ()
+ "Should unwrap with different length delimiters."
+ (let ((result (test-unwrap "<<text>>>" "<<" ">>>")))
+ (should (string= result "text"))))
+
+(ert-deftest test-unwrap-multi-char-delimiters ()
+ "Should unwrap with multi-character delimiters."
+ (let ((result (test-unwrap "BEGINdataEND" "BEGIN" "END")))
+ (should (string= result "data"))))
+
+(ert-deftest test-unwrap-space-delimiters ()
+ "Should unwrap with space delimiters."
+ (let ((result (test-unwrap " text " " " " ")))
+ (should (string= result "text"))))
+
+(ert-deftest test-unwrap-newline-delimiters ()
+ "Should unwrap with newline delimiters."
+ (let ((result (test-unwrap "\ntext\n" "\n" "\n")))
+ (should (string= result "text"))))
+
+;;; Edge Cases - Same Opening and Closing
+
+(ert-deftest test-unwrap-same-delimiters ()
+ "Should unwrap when opening and closing are the same."
+ (let ((result (test-unwrap "*text*" "*" "*")))
+ (should (string= result "text"))))
+
+(ert-deftest test-unwrap-same-multi-char ()
+ "Should unwrap same multi-char delimiters."
+ (let ((result (test-unwrap "***text***" "***" "***")))
+ (should (string= result "text"))))
+
+;;; Edge Cases - Empty Delimiters
+
+(ert-deftest test-unwrap-empty-opening ()
+ "Should handle empty opening delimiter."
+ (let ((result (test-unwrap "text)" "" ")")))
+ (should (string= result "text"))))
+
+(ert-deftest test-unwrap-empty-closing ()
+ "Should handle empty closing delimiter."
+ (let ((result (test-unwrap "(text" "(" "")))
+ (should (string= result "text"))))
+
+(ert-deftest test-unwrap-both-delimiters-empty ()
+ "Should return text unchanged when both delimiters empty."
+ (let ((result (test-unwrap "text" "" "")))
+ (should (string= result "text"))))
+
+(provide 'test-custom-text-enclose-unwrap)
+;;; test-custom-text-enclose-unwrap.el ends here
diff --git a/tests/test-custom-text-enclose-wrap.el b/tests/test-custom-text-enclose-wrap.el
new file mode 100644
index 00000000..f68a0668
--- /dev/null
+++ b/tests/test-custom-text-enclose-wrap.el
@@ -0,0 +1,240 @@
+;;; test-custom-text-enclose-wrap.el --- Tests for cj/--wrap -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; Tests for the cj/--wrap function from custom-text-enclose.el
+;;
+;; This function wraps text with different opening and closing strings.
+;; Unlike surround which uses the same string on both sides, wrap allows
+;; asymmetric delimiters.
+;;
+;; Examples:
+;; Input: "content", opening: "<div>", closing: "</div>"
+;; Output: "<div>content</div>"
+;;
+;; Input: "text", opening: "(", closing: ")"
+;; Output: "(text)"
+;;
+;; We test the NON-INTERACTIVE implementation (cj/--wrap) to avoid
+;; mocking user input. This follows our testing best practice of
+;; separating business logic from UI interaction.
+
+;;; Code:
+
+(require 'ert)
+(require 'testutil-general)
+
+;; Add modules directory to load path
+(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory))
+
+;; Stub dependencies before loading the module
+(defvar cj/custom-keymap (make-sparse-keymap)
+ "Stub keymap for testing.")
+
+;; Now load the actual production module
+(require 'custom-text-enclose)
+
+;;; Test Helpers
+
+(defun test-wrap (text opening closing)
+ "Test cj/--wrap on TEXT with OPENING and CLOSING.
+Returns the transformed string."
+ (cj/--wrap text opening closing))
+
+;;; Normal Cases - Common Bracket Types
+
+(ert-deftest test-wrap-parentheses ()
+ "Should wrap text with parentheses."
+ (let ((result (test-wrap "text" "(" ")")))
+ (should (string= result "(text)"))))
+
+(ert-deftest test-wrap-square-brackets ()
+ "Should wrap text with square brackets."
+ (let ((result (test-wrap "item" "[" "]")))
+ (should (string= result "[item]"))))
+
+(ert-deftest test-wrap-curly-braces ()
+ "Should wrap text with curly braces."
+ (let ((result (test-wrap "code" "{" "}")))
+ (should (string= result "{code}"))))
+
+(ert-deftest test-wrap-angle-brackets ()
+ "Should wrap text with angle brackets."
+ (let ((result (test-wrap "tag" "<" ">")))
+ (should (string= result "<tag>"))))
+
+;;; Normal Cases - HTML/XML Tags
+
+(ert-deftest test-wrap-html-div ()
+ "Should wrap text with HTML div tags."
+ (let ((result (test-wrap "content" "<div>" "</div>")))
+ (should (string= result "<div>content</div>"))))
+
+(ert-deftest test-wrap-html-span ()
+ "Should wrap text with HTML span tags."
+ (let ((result (test-wrap "text" "<span>" "</span>")))
+ (should (string= result "<span>text</span>"))))
+
+(ert-deftest test-wrap-xml-tag ()
+ "Should wrap text with XML tags."
+ (let ((result (test-wrap "data" "<item>" "</item>")))
+ (should (string= result "<item>data</item>"))))
+
+(ert-deftest test-wrap-html-with-attributes ()
+ "Should wrap text with HTML tag containing attributes."
+ (let ((result (test-wrap "link" "<a href=\"url\">" "</a>")))
+ (should (string= result "<a href=\"url\">link</a>"))))
+
+;;; Normal Cases - Markdown Syntax
+
+(ert-deftest test-wrap-markdown-bold ()
+ "Should wrap text with markdown bold syntax."
+ (let ((result (test-wrap "bold" "**" "**")))
+ (should (string= result "**bold**"))))
+
+(ert-deftest test-wrap-markdown-italic ()
+ "Should wrap text with markdown italic syntax."
+ (let ((result (test-wrap "italic" "*" "*")))
+ (should (string= result "*italic*"))))
+
+(ert-deftest test-wrap-markdown-code ()
+ "Should wrap text with markdown code syntax."
+ (let ((result (test-wrap "code" "`" "`")))
+ (should (string= result "`code`"))))
+
+(ert-deftest test-wrap-markdown-link ()
+ "Should wrap text with markdown link syntax."
+ (let ((result (test-wrap "text" "[" "](url)")))
+ (should (string= result "[text](url)"))))
+
+;;; Normal Cases - Various Content
+
+(ert-deftest test-wrap-single-word ()
+ "Should wrap single word."
+ (let ((result (test-wrap "word" "(" ")")))
+ (should (string= result "(word)"))))
+
+(ert-deftest test-wrap-multiple-words ()
+ "Should wrap multiple words."
+ (let ((result (test-wrap "hello world" "(" ")")))
+ (should (string= result "(hello world)"))))
+
+(ert-deftest test-wrap-sentence ()
+ "Should wrap full sentence."
+ (let ((result (test-wrap "This is a sentence." "(" ")")))
+ (should (string= result "(This is a sentence.)"))))
+
+(ert-deftest test-wrap-with-numbers ()
+ "Should wrap text with numbers."
+ (let ((result (test-wrap "123" "[" "]")))
+ (should (string= result "[123]"))))
+
+(ert-deftest test-wrap-with-special-chars ()
+ "Should wrap text with special characters."
+ (let ((result (test-wrap "hello@world.com" "<" ">")))
+ (should (string= result "<hello@world.com>"))))
+
+;;; Normal Cases - Multiline Text
+
+(ert-deftest test-wrap-multiline ()
+ "Should wrap multiline text."
+ (let ((result (test-wrap "line1\nline2\nline3" "<div>" "</div>")))
+ (should (string= result "<div>line1\nline2\nline3</div>"))))
+
+(ert-deftest test-wrap-text-with-newlines ()
+ "Should wrap text containing newlines."
+ (let ((result (test-wrap "first\nsecond" "(" ")")))
+ (should (string= result "(first\nsecond)"))))
+
+;;; Boundary Cases
+
+(ert-deftest test-wrap-empty-string ()
+ "Should wrap empty string."
+ (let ((result (test-wrap "" "(" ")")))
+ (should (string= result "()"))))
+
+(ert-deftest test-wrap-single-character ()
+ "Should wrap single character."
+ (let ((result (test-wrap "x" "[" "]")))
+ (should (string= result "[x]"))))
+
+(ert-deftest test-wrap-empty-opening ()
+ "Should handle empty opening delimiter."
+ (let ((result (test-wrap "text" "" ")")))
+ (should (string= result "text)"))))
+
+(ert-deftest test-wrap-empty-closing ()
+ "Should handle empty closing delimiter."
+ (let ((result (test-wrap "text" "(" "")))
+ (should (string= result "(text"))))
+
+(ert-deftest test-wrap-both-empty ()
+ "Should handle both delimiters empty."
+ (let ((result (test-wrap "text" "" "")))
+ (should (string= result "text"))))
+
+(ert-deftest test-wrap-very-long-text ()
+ "Should wrap very long text."
+ (let* ((long-text (make-string 1000 ?a))
+ (result (test-wrap long-text "(" ")")))
+ (should (string-prefix-p "(" result))
+ (should (string-suffix-p ")" result))
+ (should (= (length result) 1002))))
+
+(ert-deftest test-wrap-whitespace-only ()
+ "Should wrap whitespace-only text."
+ (let ((result (test-wrap " " "(" ")")))
+ (should (string= result "( )"))))
+
+(ert-deftest test-wrap-tabs ()
+ "Should wrap text with tabs."
+ (let ((result (test-wrap "\t\ttext\t\t" "[" "]")))
+ (should (string= result "[\t\ttext\t\t]"))))
+
+;;; Edge Cases - Already Wrapped
+
+(ert-deftest test-wrap-already-wrapped ()
+ "Should wrap text that is already wrapped."
+ (let ((result (test-wrap "(hello)" "[" "]")))
+ (should (string= result "[(hello)]"))))
+
+(ert-deftest test-wrap-nested ()
+ "Should wrap text creating nested delimiters."
+ (let ((result (test-wrap "[inner]" "(" ")")))
+ (should (string= result "([inner])"))))
+
+;;; Edge Cases - Special Delimiters
+
+(ert-deftest test-wrap-asymmetric-length ()
+ "Should wrap with different length delimiters."
+ (let ((result (test-wrap "text" "<<" ">>>")))
+ (should (string= result "<<text>>>"))))
+
+(ert-deftest test-wrap-multi-char-delimiters ()
+ "Should wrap with multi-character delimiters."
+ (let ((result (test-wrap "data" "BEGIN" "END")))
+ (should (string= result "BEGINdataEND"))))
+
+(ert-deftest test-wrap-space-delimiters ()
+ "Should wrap with space delimiters."
+ (let ((result (test-wrap "text" " " " ")))
+ (should (string= result " text "))))
+
+(ert-deftest test-wrap-newline-delimiters ()
+ "Should wrap with newline delimiters."
+ (let ((result (test-wrap "text" "\n" "\n")))
+ (should (string= result "\ntext\n"))))
+
+(ert-deftest test-wrap-quote-delimiters ()
+ "Should wrap with quote delimiters."
+ (let ((result (test-wrap "text" "\"" "\"")))
+ (should (string= result "\"text\""))))
+
+;;; Edge Cases - Same Opening and Closing
+
+(ert-deftest test-wrap-same-delimiters ()
+ "Should work like surround when delimiters are the same."
+ (let ((result (test-wrap "text" "*" "*")))
+ (should (string= result "*text*"))))
+
+(provide 'test-custom-text-enclose-wrap)
+;;; test-custom-text-enclose-wrap.el ends here
diff --git a/tests/test-custom-whitespace-collapse.el b/tests/test-custom-whitespace-collapse.el
new file mode 100644
index 00000000..40face95
--- /dev/null
+++ b/tests/test-custom-whitespace-collapse.el
@@ -0,0 +1,150 @@
+;;; test-custom-whitespace-collapse.el --- Tests for cj/--collapse-whitespace -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; Tests for the cj/--collapse-whitespace function from custom-whitespace.el
+;;
+;; This function collapses whitespace in text by:
+;; - Converting all tabs to spaces
+;; - Removing leading and trailing whitespace
+;; - Collapsing multiple consecutive spaces to single space
+;; - Preserving newlines and text structure
+;;
+;; We test the NON-INTERACTIVE implementation (cj/--collapse-whitespace)
+;; to avoid mocking region selection. This follows our testing best practice
+;; of separating business logic from UI interaction.
+
+;;; Code:
+
+(require 'ert)
+(require 'testutil-general)
+
+;; Add modules directory to load path
+(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory))
+
+;; Stub dependencies before loading the module
+(defvar cj/custom-keymap (make-sparse-keymap)
+ "Stub keymap for testing.")
+
+;; Now load the actual production module
+(require 'custom-whitespace)
+
+;;; Test Helpers
+
+(defun test-collapse-whitespace (input-text)
+ "Test cj/--collapse-whitespace on INPUT-TEXT.
+Returns the buffer string after operation."
+ (with-temp-buffer
+ (insert input-text)
+ (cj/--collapse-whitespace (point-min) (point-max))
+ (buffer-string)))
+
+;;; Normal Cases
+
+(ert-deftest test-collapse-whitespace-multiple-spaces ()
+ "Should collapse multiple spaces to single space."
+ (let ((result (test-collapse-whitespace "hello world")))
+ (should (string= result "hello world"))))
+
+(ert-deftest test-collapse-whitespace-multiple-tabs ()
+ "Should convert tabs to spaces and collapse."
+ (let ((result (test-collapse-whitespace "hello\t\t\tworld")))
+ (should (string= result "hello world"))))
+
+(ert-deftest test-collapse-whitespace-mixed-tabs-spaces ()
+ "Should handle mixed tabs and spaces."
+ (let ((result (test-collapse-whitespace "hello \t \t world")))
+ (should (string= result "hello world"))))
+
+(ert-deftest test-collapse-whitespace-leading-trailing ()
+ "Should remove leading and trailing whitespace."
+ (let ((result (test-collapse-whitespace " hello world ")))
+ (should (string= result "hello world"))))
+
+(ert-deftest test-collapse-whitespace-tabs-leading-trailing ()
+ "Should remove leading and trailing tabs."
+ (let ((result (test-collapse-whitespace "\t\thello world\t\t")))
+ (should (string= result "hello world"))))
+
+(ert-deftest test-collapse-whitespace-multiple-words ()
+ "Should collapse spaces between multiple words."
+ (let ((result (test-collapse-whitespace "one two three four")))
+ (should (string= result "one two three four"))))
+
+(ert-deftest test-collapse-whitespace-preserve-newlines ()
+ "Should preserve newlines while collapsing spaces."
+ (let ((result (test-collapse-whitespace "hello world\nfoo bar")))
+ (should (string= result "hello world\nfoo bar"))))
+
+(ert-deftest test-collapse-whitespace-multiple-lines ()
+ "Should handle multiple lines with various whitespace."
+ (let ((result (test-collapse-whitespace " hello world \n\t\tfoo bar\t\t")))
+ (should (string= result "hello world\nfoo bar"))))
+
+;;; Boundary Cases
+
+(ert-deftest test-collapse-whitespace-empty-string ()
+ "Should handle empty string."
+ (let ((result (test-collapse-whitespace "")))
+ (should (string= result ""))))
+
+(ert-deftest test-collapse-whitespace-single-char ()
+ "Should handle single character with surrounding spaces."
+ (let ((result (test-collapse-whitespace " x ")))
+ (should (string= result "x"))))
+
+(ert-deftest test-collapse-whitespace-only-whitespace ()
+ "Should handle text with only whitespace (becomes empty)."
+ (let ((result (test-collapse-whitespace " \t \t ")))
+ (should (string= result ""))))
+
+(ert-deftest test-collapse-whitespace-no-extra-whitespace ()
+ "Should handle text with no extra whitespace (no-op)."
+ (let ((result (test-collapse-whitespace "hello world")))
+ (should (string= result "hello world"))))
+
+(ert-deftest test-collapse-whitespace-single-space ()
+ "Should handle text with already-collapsed spaces (no-op)."
+ (let ((result (test-collapse-whitespace "one two three")))
+ (should (string= result "one two three"))))
+
+(ert-deftest test-collapse-whitespace-very-long-line ()
+ "Should handle very long lines with many spaces."
+ (let ((result (test-collapse-whitespace "word word word word word")))
+ (should (string= result "word word word word word"))))
+
+(ert-deftest test-collapse-whitespace-multiple-newlines ()
+ "Should preserve multiple newlines while removing spaces."
+ (let ((result (test-collapse-whitespace "hello world\n\n\nfoo bar")))
+ (should (string= result "hello world\n\n\nfoo bar"))))
+
+(ert-deftest test-collapse-whitespace-spaces-around-newlines ()
+ "Should remove spaces before/after newlines."
+ (let ((result (test-collapse-whitespace "hello \n world")))
+ (should (string= result "hello\nworld"))))
+
+(ert-deftest test-collapse-whitespace-empty-lines ()
+ "Should handle empty lines (lines become empty after whitespace removal)."
+ (let ((result (test-collapse-whitespace "line1\n \nline2")))
+ (should (string= result "line1\n\nline2"))))
+
+;;; Error Cases
+
+(ert-deftest test-collapse-whitespace-start-greater-than-end ()
+ "Should error when start > end."
+ (should-error
+ (with-temp-buffer
+ (insert "hello world")
+ (cj/--collapse-whitespace (point-max) (point-min)))
+ :type 'error))
+
+(ert-deftest test-collapse-whitespace-empty-region ()
+ "Should handle empty region (start == end) without error."
+ (with-temp-buffer
+ (insert "hello world")
+ (let ((pos (/ (+ (point-min) (point-max)) 2)))
+ (cj/--collapse-whitespace pos pos)
+ ;; Should complete without error and not change buffer
+ (should (string= (buffer-string) "hello world")))))
+
+(provide 'test-custom-whitespace-collapse)
+;;; test-custom-whitespace-collapse.el ends here
diff --git a/tests/test-custom-whitespace-delete-all.el b/tests/test-custom-whitespace-delete-all.el
new file mode 100644
index 00000000..00abb1d4
--- /dev/null
+++ b/tests/test-custom-whitespace-delete-all.el
@@ -0,0 +1,150 @@
+;;; test-custom-whitespace-delete-all.el --- Tests for cj/--delete-all-whitespace -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; Tests for the cj/--delete-all-whitespace function from custom-whitespace.el
+;;
+;; This function removes ALL whitespace characters from the region:
+;; spaces, tabs, newlines, and carriage returns. Useful for creating
+;; compact identifiers or removing all formatting.
+;;
+;; Uses the regexp [ \t\n\r]+ to match all whitespace.
+;;
+;; We test the NON-INTERACTIVE implementation (cj/--delete-all-whitespace)
+;; to avoid mocking region selection. This follows our testing best practice
+;; of separating business logic from UI interaction.
+
+;;; Code:
+
+(require 'ert)
+(require 'testutil-general)
+
+;; Add modules directory to load path
+(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory))
+
+;; Stub dependencies before loading the module
+(defvar cj/custom-keymap (make-sparse-keymap)
+ "Stub keymap for testing.")
+
+;; Now load the actual production module
+(require 'custom-whitespace)
+
+;;; Test Helpers
+
+(defun test-delete-all-whitespace (input-text)
+ "Test cj/--delete-all-whitespace on INPUT-TEXT.
+Returns the buffer string after operation."
+ (with-temp-buffer
+ (insert input-text)
+ (cj/--delete-all-whitespace (point-min) (point-max))
+ (buffer-string)))
+
+;;; Normal Cases
+
+(ert-deftest test-delete-all-whitespace-single-space ()
+ "Should remove single space."
+ (let ((result (test-delete-all-whitespace "hello world")))
+ (should (string= result "helloworld"))))
+
+(ert-deftest test-delete-all-whitespace-multiple-spaces ()
+ "Should remove multiple spaces."
+ (let ((result (test-delete-all-whitespace "hello world")))
+ (should (string= result "helloworld"))))
+
+(ert-deftest test-delete-all-whitespace-tabs ()
+ "Should remove tabs."
+ (let ((result (test-delete-all-whitespace "hello\tworld")))
+ (should (string= result "helloworld"))))
+
+(ert-deftest test-delete-all-whitespace-newlines ()
+ "Should remove newlines (joining lines)."
+ (let ((result (test-delete-all-whitespace "hello\nworld")))
+ (should (string= result "helloworld"))))
+
+(ert-deftest test-delete-all-whitespace-mixed ()
+ "Should remove all types of whitespace."
+ (let ((result (test-delete-all-whitespace "hello \t\n world")))
+ (should (string= result "helloworld"))))
+
+(ert-deftest test-delete-all-whitespace-multiple-words ()
+ "Should remove whitespace from multiple words."
+ (let ((result (test-delete-all-whitespace "one two three four")))
+ (should (string= result "onetwothreefour"))))
+
+(ert-deftest test-delete-all-whitespace-multiline ()
+ "Should remove all whitespace across multiple lines."
+ (let ((result (test-delete-all-whitespace "line1\nline2\nline3")))
+ (should (string= result "line1line2line3"))))
+
+(ert-deftest test-delete-all-whitespace-leading-trailing ()
+ "Should remove leading and trailing whitespace."
+ (let ((result (test-delete-all-whitespace " hello world ")))
+ (should (string= result "helloworld"))))
+
+(ert-deftest test-delete-all-whitespace-carriage-returns ()
+ "Should handle carriage returns."
+ (let ((result (test-delete-all-whitespace "hello\r\nworld")))
+ (should (string= result "helloworld"))))
+
+;;; Boundary Cases
+
+(ert-deftest test-delete-all-whitespace-empty-string ()
+ "Should handle empty string."
+ (let ((result (test-delete-all-whitespace "")))
+ (should (string= result ""))))
+
+(ert-deftest test-delete-all-whitespace-no-whitespace ()
+ "Should handle text with no whitespace (no-op)."
+ (let ((result (test-delete-all-whitespace "helloworld")))
+ (should (string= result "helloworld"))))
+
+(ert-deftest test-delete-all-whitespace-only-whitespace ()
+ "Should delete all content when only whitespace exists."
+ (let ((result (test-delete-all-whitespace " \t \n ")))
+ (should (string= result ""))))
+
+(ert-deftest test-delete-all-whitespace-single-char ()
+ "Should handle single character with surrounding whitespace."
+ (let ((result (test-delete-all-whitespace " x ")))
+ (should (string= result "x"))))
+
+(ert-deftest test-delete-all-whitespace-very-long-text ()
+ "Should handle very long text."
+ (let ((result (test-delete-all-whitespace "word word word word word word word word")))
+ (should (string= result "wordwordwordwordwordwordwordword"))))
+
+(ert-deftest test-delete-all-whitespace-single-whitespace ()
+ "Should delete single whitespace character."
+ (let ((result (test-delete-all-whitespace " ")))
+ (should (string= result ""))))
+
+(ert-deftest test-delete-all-whitespace-consecutive-newlines ()
+ "Should remove all consecutive newlines."
+ (let ((result (test-delete-all-whitespace "hello\n\n\nworld")))
+ (should (string= result "helloworld"))))
+
+(ert-deftest test-delete-all-whitespace-complex-structure ()
+ "Should handle complex whitespace patterns."
+ (let ((result (test-delete-all-whitespace " hello\n\t world \n foo\t\tbar ")))
+ (should (string= result "helloworldfoobar"))))
+
+;;; Error Cases
+
+(ert-deftest test-delete-all-whitespace-start-greater-than-end ()
+ "Should error when start > end."
+ (should-error
+ (with-temp-buffer
+ (insert "hello world")
+ (cj/--delete-all-whitespace (point-max) (point-min)))
+ :type 'error))
+
+(ert-deftest test-delete-all-whitespace-empty-region ()
+ "Should handle empty region (start == end) without error."
+ (with-temp-buffer
+ (insert "hello world")
+ (let ((pos (/ (+ (point-min) (point-max)) 2)))
+ (cj/--delete-all-whitespace pos pos)
+ ;; Should complete without error and not change buffer
+ (should (string= (buffer-string) "hello world")))))
+
+(provide 'test-custom-whitespace-delete-all)
+;;; test-custom-whitespace-delete-all.el ends here
diff --git a/tests/test-custom-whitespace-delete-blank-lines.el b/tests/test-custom-whitespace-delete-blank-lines.el
new file mode 100644
index 00000000..2d250521
--- /dev/null
+++ b/tests/test-custom-whitespace-delete-blank-lines.el
@@ -0,0 +1,146 @@
+;;; test-custom-whitespace-delete-blank-lines.el --- Tests for cj/--delete-blank-lines -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; Tests for the cj/--delete-blank-lines function from custom-whitespace.el
+;;
+;; This function deletes blank lines from text, where blank lines are defined
+;; as lines containing only whitespace (spaces, tabs) or nothing at all.
+;; Uses the regexp ^[[:space:]]*$ to match blank lines.
+;;
+;; We test the NON-INTERACTIVE implementation (cj/--delete-blank-lines)
+;; to avoid mocking user prompts. This follows our testing best practice
+;; of separating business logic from UI interaction.
+
+;;; Code:
+
+(require 'ert)
+(require 'testutil-general)
+
+;; Add modules directory to load path
+(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory))
+
+;; Stub dependencies before loading the module
+(defvar cj/custom-keymap (make-sparse-keymap)
+ "Stub keymap for testing.")
+
+;; Now load the actual production module
+(require 'custom-whitespace)
+
+;;; Test Helpers
+
+(defun test-delete-blank-lines (input-text)
+ "Test cj/--delete-blank-lines on INPUT-TEXT.
+Returns the buffer string after operation."
+ (with-temp-buffer
+ (insert input-text)
+ (cj/--delete-blank-lines (point-min) (point-max))
+ (buffer-string)))
+
+;;; Normal Cases
+
+(ert-deftest test-delete-blank-lines-single-blank ()
+ "Should delete single blank line between text."
+ (let ((result (test-delete-blank-lines "line1\n\nline2")))
+ (should (string= result "line1\nline2"))))
+
+(ert-deftest test-delete-blank-lines-multiple-consecutive ()
+ "Should delete multiple consecutive blank lines."
+ (let ((result (test-delete-blank-lines "line1\n\n\n\nline2")))
+ (should (string= result "line1\nline2"))))
+
+(ert-deftest test-delete-blank-lines-spaces-only ()
+ "Should delete lines with spaces only."
+ (let ((result (test-delete-blank-lines "line1\n \nline2")))
+ (should (string= result "line1\nline2"))))
+
+(ert-deftest test-delete-blank-lines-tabs-only ()
+ "Should delete lines with tabs only."
+ (let ((result (test-delete-blank-lines "line1\n\t\t\nline2")))
+ (should (string= result "line1\nline2"))))
+
+(ert-deftest test-delete-blank-lines-mixed-whitespace ()
+ "Should delete lines with mixed whitespace."
+ (let ((result (test-delete-blank-lines "line1\n \t \t \nline2")))
+ (should (string= result "line1\nline2"))))
+
+(ert-deftest test-delete-blank-lines-no-blank-lines ()
+ "Should handle text with no blank lines (no-op)."
+ (let ((result (test-delete-blank-lines "line1\nline2\nline3")))
+ (should (string= result "line1\nline2\nline3"))))
+
+(ert-deftest test-delete-blank-lines-at-start ()
+ "Should delete blank lines at start of region."
+ (let ((result (test-delete-blank-lines "\n\nline1\nline2")))
+ (should (string= result "line1\nline2"))))
+
+(ert-deftest test-delete-blank-lines-at-end ()
+ "Should delete blank lines at end of region."
+ (let ((result (test-delete-blank-lines "line1\nline2\n\n")))
+ (should (string= result "line1\nline2\n"))))
+
+(ert-deftest test-delete-blank-lines-scattered ()
+ "Should delete blank lines scattered throughout text."
+ (let ((result (test-delete-blank-lines "line1\n\nline2\n \nline3\n\t\nline4")))
+ (should (string= result "line1\nline2\nline3\nline4"))))
+
+;;; Boundary Cases
+
+(ert-deftest test-delete-blank-lines-empty-string ()
+ "Should handle empty string."
+ (let ((result (test-delete-blank-lines "")))
+ (should (string= result ""))))
+
+(ert-deftest test-delete-blank-lines-only-blank-lines ()
+ "Should delete all lines if only blank lines exist."
+ (let ((result (test-delete-blank-lines "\n\n\n")))
+ (should (string= result ""))))
+
+(ert-deftest test-delete-blank-lines-only-whitespace ()
+ "Should delete lines containing only whitespace."
+ (let ((result (test-delete-blank-lines " \n\t\t\n \t ")))
+ (should (string= result ""))))
+
+(ert-deftest test-delete-blank-lines-single-line-content ()
+ "Should handle single line with content (no-op)."
+ (let ((result (test-delete-blank-lines "hello world")))
+ (should (string= result "hello world"))))
+
+(ert-deftest test-delete-blank-lines-single-blank-line ()
+ "Should delete single blank line."
+ (let ((result (test-delete-blank-lines "\n")))
+ (should (string= result ""))))
+
+(ert-deftest test-delete-blank-lines-very-long-region ()
+ "Should handle very long region with many blank lines."
+ (let* ((lines (make-list 100 "content"))
+ (input (mapconcat #'identity lines "\n\n"))
+ (expected (mapconcat #'identity lines "\n"))
+ (result (test-delete-blank-lines input)))
+ (should (string= result expected))))
+
+(ert-deftest test-delete-blank-lines-preserve-content-lines ()
+ "Should preserve lines with any non-whitespace content."
+ (let ((result (test-delete-blank-lines "x\n\ny\n \nz")))
+ (should (string= result "x\ny\nz"))))
+
+;;; Error Cases
+
+(ert-deftest test-delete-blank-lines-start-greater-than-end ()
+ "Should error when start > end."
+ (should-error
+ (with-temp-buffer
+ (insert "line1\n\nline2")
+ (cj/--delete-blank-lines (point-max) (point-min)))
+ :type 'error))
+
+(ert-deftest test-delete-blank-lines-empty-region ()
+ "Should handle empty region (start == end) without error."
+ (with-temp-buffer
+ (insert "line1\n\nline2")
+ (let ((pos (/ (+ (point-min) (point-max)) 2)))
+ (cj/--delete-blank-lines pos pos)
+ ;; Should complete without error
+ (should (string-match-p "line1" (buffer-string))))))
+
+(provide 'test-custom-whitespace-delete-blank-lines)
+;;; test-custom-whitespace-delete-blank-lines.el ends here
diff --git a/tests/test-custom-whitespace-ensure-single-blank.el b/tests/test-custom-whitespace-ensure-single-blank.el
new file mode 100644
index 00000000..7cd03e79
--- /dev/null
+++ b/tests/test-custom-whitespace-ensure-single-blank.el
@@ -0,0 +1,146 @@
+;;; test-custom-whitespace-ensure-single-blank.el --- Tests for cj/--ensure-single-blank-line -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; Tests for the cj/--ensure-single-blank-line function from custom-whitespace.el
+;;
+;; This function collapses multiple consecutive blank lines to exactly one blank line.
+;; Different from delete-blank-lines which removes ALL blank lines, this function
+;; preserves blank lines but ensures no more than one blank line appears consecutively.
+;;
+;; A blank line is defined as a line containing only whitespace (spaces, tabs) or nothing.
+;; Uses the regexp (^[[:space:]]*$\n){2,} to match 2+ consecutive blank lines.
+;;
+;; We test the NON-INTERACTIVE implementation (cj/--ensure-single-blank-line)
+;; to avoid mocking user prompts. This follows our testing best practice
+;; of separating business logic from UI interaction.
+
+;;; Code:
+
+(require 'ert)
+(require 'testutil-general)
+
+;; Add modules directory to load path
+(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory))
+
+;; Stub dependencies before loading the module
+(defvar cj/custom-keymap (make-sparse-keymap)
+ "Stub keymap for testing.")
+
+;; Now load the actual production module
+(require 'custom-whitespace)
+
+;;; Test Helpers
+
+(defun test-ensure-single-blank-line (input-text)
+ "Test cj/--ensure-single-blank-line on INPUT-TEXT.
+Returns the buffer string after operation."
+ (with-temp-buffer
+ (insert input-text)
+ (cj/--ensure-single-blank-line (point-min) (point-max))
+ (buffer-string)))
+
+;;; Normal Cases
+
+(ert-deftest test-ensure-single-blank-two-blanks ()
+ "Should collapse two blank lines to one."
+ (let ((result (test-ensure-single-blank-line "line1\n\n\nline2")))
+ (should (string= result "line1\n\nline2"))))
+
+(ert-deftest test-ensure-single-blank-three-blanks ()
+ "Should collapse three blank lines to one."
+ (let ((result (test-ensure-single-blank-line "line1\n\n\n\nline2")))
+ (should (string= result "line1\n\nline2"))))
+
+(ert-deftest test-ensure-single-blank-many-blanks ()
+ "Should collapse many blank lines to one."
+ (let ((result (test-ensure-single-blank-line "line1\n\n\n\n\n\n\nline2")))
+ (should (string= result "line1\n\nline2"))))
+
+(ert-deftest test-ensure-single-blank-preserve-single ()
+ "Should preserve single blank lines (no-op)."
+ (let ((result (test-ensure-single-blank-line "line1\n\nline2")))
+ (should (string= result "line1\n\nline2"))))
+
+(ert-deftest test-ensure-single-blank-multiple-groups ()
+ "Should handle multiple groups of consecutive blanks."
+ (let ((result (test-ensure-single-blank-line "line1\n\n\nline2\n\n\n\nline3")))
+ (should (string= result "line1\n\nline2\n\nline3"))))
+
+(ert-deftest test-ensure-single-blank-blanks-with-spaces ()
+ "Should handle blank lines with spaces only."
+ (let ((result (test-ensure-single-blank-line "line1\n \n \nline2")))
+ (should (string= result "line1\n\nline2"))))
+
+(ert-deftest test-ensure-single-blank-blanks-with-tabs ()
+ "Should handle blank lines with tabs only."
+ (let ((result (test-ensure-single-blank-line "line1\n\t\t\n\t\t\nline2")))
+ (should (string= result "line1\n\nline2"))))
+
+(ert-deftest test-ensure-single-blank-mixed-whitespace ()
+ "Should handle blank lines with mixed whitespace."
+ (let ((result (test-ensure-single-blank-line "line1\n \t \n \t \nline2")))
+ (should (string= result "line1\n\nline2"))))
+
+(ert-deftest test-ensure-single-blank-no-blanks ()
+ "Should handle text with no blank lines (no-op)."
+ (let ((result (test-ensure-single-blank-line "line1\nline2\nline3")))
+ (should (string= result "line1\nline2\nline3"))))
+
+;;; Boundary Cases
+
+(ert-deftest test-ensure-single-blank-empty-string ()
+ "Should handle empty string."
+ (let ((result (test-ensure-single-blank-line "")))
+ (should (string= result ""))))
+
+(ert-deftest test-ensure-single-blank-only-blanks ()
+ "Should collapse many blank lines to one blank line."
+ (let ((result (test-ensure-single-blank-line "\n\n\n\n")))
+ (should (string= result "\n\n"))))
+
+(ert-deftest test-ensure-single-blank-at-start ()
+ "Should collapse multiple blank lines at start to one."
+ (let ((result (test-ensure-single-blank-line "\n\n\nline1")))
+ (should (string= result "\n\nline1"))))
+
+(ert-deftest test-ensure-single-blank-at-end ()
+ "Should collapse multiple blank lines at end to one."
+ (let ((result (test-ensure-single-blank-line "line1\n\n\n")))
+ (should (string= result "line1\n\n"))))
+
+(ert-deftest test-ensure-single-blank-single-line ()
+ "Should handle single line (no-op)."
+ (let ((result (test-ensure-single-blank-line "line1")))
+ (should (string= result "line1"))))
+
+(ert-deftest test-ensure-single-blank-complex-structure ()
+ "Should handle complex mix of content and blanks."
+ (let ((result (test-ensure-single-blank-line "line1\n\n\nline2\nline3\n\n\n\nline4")))
+ (should (string= result "line1\n\nline2\nline3\n\nline4"))))
+
+(ert-deftest test-ensure-single-blank-preserves-content ()
+ "Should not modify lines with content."
+ (let ((result (test-ensure-single-blank-line " line1 \n\n\n line2 ")))
+ (should (string= result " line1 \n\n line2 "))))
+
+;;; Error Cases
+
+(ert-deftest test-ensure-single-blank-start-greater-than-end ()
+ "Should error when start > end."
+ (should-error
+ (with-temp-buffer
+ (insert "line1\n\n\nline2")
+ (cj/--ensure-single-blank-line (point-max) (point-min)))
+ :type 'error))
+
+(ert-deftest test-ensure-single-blank-empty-region ()
+ "Should handle empty region (start == end) without error."
+ (with-temp-buffer
+ (insert "line1\n\n\nline2")
+ (let ((pos (/ (+ (point-min) (point-max)) 2)))
+ (cj/--ensure-single-blank-line pos pos)
+ ;; Should complete without error
+ (should (string-match-p "line1" (buffer-string))))))
+
+(provide 'test-custom-whitespace-ensure-single-blank)
+;;; test-custom-whitespace-ensure-single-blank.el ends here
diff --git a/tests/test-custom-whitespace-hyphenate.el b/tests/test-custom-whitespace-hyphenate.el
new file mode 100644
index 00000000..03462fab
--- /dev/null
+++ b/tests/test-custom-whitespace-hyphenate.el
@@ -0,0 +1,140 @@
+;;; test-custom-whitespace-hyphenate.el --- Tests for cj/--hyphenate-whitespace -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; Tests for the cj/--hyphenate-whitespace function from custom-whitespace.el
+;;
+;; This function replaces all runs of whitespace (spaces, tabs, newlines,
+;; carriage returns) with single hyphens. Useful for converting text with
+;; whitespace into hyphenated identifiers or URLs.
+;;
+;; Uses the regexp [ \t\n\r]+ to match whitespace runs.
+;;
+;; We test the NON-INTERACTIVE implementation (cj/--hyphenate-whitespace)
+;; to avoid mocking region selection. This follows our testing best practice
+;; of separating business logic from UI interaction.
+
+;;; Code:
+
+(require 'ert)
+(require 'testutil-general)
+
+;; Add modules directory to load path
+(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory))
+
+;; Stub dependencies before loading the module
+(defvar cj/custom-keymap (make-sparse-keymap)
+ "Stub keymap for testing.")
+
+;; Now load the actual production module
+(require 'custom-whitespace)
+
+;;; Test Helpers
+
+(defun test-hyphenate-whitespace (input-text)
+ "Test cj/--hyphenate-whitespace on INPUT-TEXT.
+Returns the buffer string after operation."
+ (with-temp-buffer
+ (insert input-text)
+ (cj/--hyphenate-whitespace (point-min) (point-max))
+ (buffer-string)))
+
+;;; Normal Cases
+
+(ert-deftest test-hyphenate-whitespace-single-space ()
+ "Should replace single space with hyphen."
+ (let ((result (test-hyphenate-whitespace "hello world")))
+ (should (string= result "hello-world"))))
+
+(ert-deftest test-hyphenate-whitespace-multiple-spaces ()
+ "Should replace multiple spaces with single hyphen."
+ (let ((result (test-hyphenate-whitespace "hello world")))
+ (should (string= result "hello-world"))))
+
+(ert-deftest test-hyphenate-whitespace-tabs ()
+ "Should replace tabs with hyphen."
+ (let ((result (test-hyphenate-whitespace "hello\tworld")))
+ (should (string= result "hello-world"))))
+
+(ert-deftest test-hyphenate-whitespace-mixed-tabs-spaces ()
+ "Should replace mixed tabs and spaces with single hyphen."
+ (let ((result (test-hyphenate-whitespace "hello \t world")))
+ (should (string= result "hello-world"))))
+
+(ert-deftest test-hyphenate-whitespace-newlines ()
+ "Should replace newlines with hyphen (joining lines)."
+ (let ((result (test-hyphenate-whitespace "hello\nworld")))
+ (should (string= result "hello-world"))))
+
+(ert-deftest test-hyphenate-whitespace-multiple-newlines ()
+ "Should replace multiple newlines with single hyphen."
+ (let ((result (test-hyphenate-whitespace "hello\n\n\nworld")))
+ (should (string= result "hello-world"))))
+
+(ert-deftest test-hyphenate-whitespace-multiple-words ()
+ "Should hyphenate multiple words with various whitespace."
+ (let ((result (test-hyphenate-whitespace "one two three\tfour\nfive")))
+ (should (string= result "one-two-three-four-five"))))
+
+(ert-deftest test-hyphenate-whitespace-carriage-returns ()
+ "Should handle carriage returns."
+ (let ((result (test-hyphenate-whitespace "hello\r\nworld")))
+ (should (string= result "hello-world"))))
+
+;;; Boundary Cases
+
+(ert-deftest test-hyphenate-whitespace-empty-string ()
+ "Should handle empty string."
+ (let ((result (test-hyphenate-whitespace "")))
+ (should (string= result ""))))
+
+(ert-deftest test-hyphenate-whitespace-no-whitespace ()
+ "Should handle text with no whitespace (no-op)."
+ (let ((result (test-hyphenate-whitespace "helloworld")))
+ (should (string= result "helloworld"))))
+
+(ert-deftest test-hyphenate-whitespace-only-whitespace ()
+ "Should convert text with only whitespace to single hyphen."
+ (let ((result (test-hyphenate-whitespace " \t \n ")))
+ (should (string= result "-"))))
+
+(ert-deftest test-hyphenate-whitespace-single-char ()
+ "Should handle single character with surrounding spaces."
+ (let ((result (test-hyphenate-whitespace " x ")))
+ (should (string= result "-x-"))))
+
+(ert-deftest test-hyphenate-whitespace-very-long-text ()
+ "Should handle very long text with many spaces."
+ (let ((result (test-hyphenate-whitespace "word word word word word word word word")))
+ (should (string= result "word-word-word-word-word-word-word-word"))))
+
+(ert-deftest test-hyphenate-whitespace-leading-whitespace ()
+ "Should replace leading whitespace with hyphen."
+ (let ((result (test-hyphenate-whitespace " hello world")))
+ (should (string= result "-hello-world"))))
+
+(ert-deftest test-hyphenate-whitespace-trailing-whitespace ()
+ "Should replace trailing whitespace with hyphen."
+ (let ((result (test-hyphenate-whitespace "hello world ")))
+ (should (string= result "hello-world-"))))
+
+;;; Error Cases
+
+(ert-deftest test-hyphenate-whitespace-start-greater-than-end ()
+ "Should error when start > end."
+ (should-error
+ (with-temp-buffer
+ (insert "hello world")
+ (cj/--hyphenate-whitespace (point-max) (point-min)))
+ :type 'error))
+
+(ert-deftest test-hyphenate-whitespace-empty-region ()
+ "Should handle empty region (start == end) without error."
+ (with-temp-buffer
+ (insert "hello world")
+ (let ((pos (/ (+ (point-min) (point-max)) 2)))
+ (cj/--hyphenate-whitespace pos pos)
+ ;; Should complete without error and not change buffer
+ (should (string= (buffer-string) "hello world")))))
+
+(provide 'test-custom-whitespace-hyphenate)
+;;; test-custom-whitespace-hyphenate.el ends here
diff --git a/tests/test-custom-whitespace-remove-leading-trailing.el b/tests/test-custom-whitespace-remove-leading-trailing.el
new file mode 100644
index 00000000..5a846e7f
--- /dev/null
+++ b/tests/test-custom-whitespace-remove-leading-trailing.el
@@ -0,0 +1,157 @@
+;;; test-custom-whitespace-remove-leading-trailing.el --- Tests for cj/--remove-leading-trailing-whitespace -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; Tests for the cj/--remove-leading-trailing-whitespace function from custom-whitespace.el
+;;
+;; This function removes leading and trailing whitespace (spaces and tabs) from text.
+;; - Removes leading whitespace: ^[ \t]+
+;; - Removes trailing whitespace: [ \t]+$
+;; - Preserves interior whitespace
+;; - Operates on any region defined by START and END
+;;
+;; We test the NON-INTERACTIVE implementation (cj/--remove-leading-trailing-whitespace)
+;; to avoid mocking region selection and prefix arguments. This follows our testing
+;; best practice of separating business logic from UI interaction.
+
+;;; Code:
+
+(require 'ert)
+(require 'testutil-general)
+
+;; Add modules directory to load path
+(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory))
+
+;; Stub dependencies before loading the module
+(defvar cj/custom-keymap (make-sparse-keymap)
+ "Stub keymap for testing.")
+
+;; Now load the actual production module
+(require 'custom-whitespace)
+
+;;; Test Helpers
+
+(defun test-remove-leading-trailing (input-text)
+ "Test cj/--remove-leading-trailing-whitespace on INPUT-TEXT.
+Returns the buffer string after operation."
+ (with-temp-buffer
+ (insert input-text)
+ (cj/--remove-leading-trailing-whitespace (point-min) (point-max))
+ (buffer-string)))
+
+;;; Normal Cases
+
+(ert-deftest test-remove-leading-trailing-leading-spaces ()
+ "Should remove leading spaces from single line."
+ (let ((result (test-remove-leading-trailing " hello world")))
+ (should (string= result "hello world"))))
+
+(ert-deftest test-remove-leading-trailing-trailing-spaces ()
+ "Should remove trailing spaces from single line."
+ (let ((result (test-remove-leading-trailing "hello world ")))
+ (should (string= result "hello world"))))
+
+(ert-deftest test-remove-leading-trailing-both-spaces ()
+ "Should remove both leading and trailing spaces."
+ (let ((result (test-remove-leading-trailing " hello world ")))
+ (should (string= result "hello world"))))
+
+(ert-deftest test-remove-leading-trailing-leading-tabs ()
+ "Should remove leading tabs from single line."
+ (let ((result (test-remove-leading-trailing "\t\thello world")))
+ (should (string= result "hello world"))))
+
+(ert-deftest test-remove-leading-trailing-trailing-tabs ()
+ "Should remove trailing tabs from single line."
+ (let ((result (test-remove-leading-trailing "hello world\t\t")))
+ (should (string= result "hello world"))))
+
+(ert-deftest test-remove-leading-trailing-mixed-tabs-spaces ()
+ "Should remove mixed tabs and spaces."
+ (let ((result (test-remove-leading-trailing " \t hello world \t ")))
+ (should (string= result "hello world"))))
+
+(ert-deftest test-remove-leading-trailing-preserve-interior ()
+ "Should preserve interior whitespace."
+ (let ((result (test-remove-leading-trailing " hello world \t")))
+ (should (string= result "hello world"))))
+
+(ert-deftest test-remove-leading-trailing-multiple-lines ()
+ "Should handle multiple lines with leading/trailing whitespace."
+ (let ((result (test-remove-leading-trailing " line1 \n\t\tline2\t\n line3 ")))
+ (should (string= result "line1\nline2\nline3"))))
+
+(ert-deftest test-remove-leading-trailing-multiline-preserve-interior ()
+ "Should preserve interior whitespace on multiple lines."
+ (let ((result (test-remove-leading-trailing " hello world \n foo bar ")))
+ (should (string= result "hello world\nfoo bar"))))
+
+;;; Boundary Cases
+
+(ert-deftest test-remove-leading-trailing-empty-string ()
+ "Should handle empty string."
+ (let ((result (test-remove-leading-trailing "")))
+ (should (string= result ""))))
+
+(ert-deftest test-remove-leading-trailing-single-char ()
+ "Should handle single character with surrounding spaces."
+ (let ((result (test-remove-leading-trailing " x ")))
+ (should (string= result "x"))))
+
+(ert-deftest test-remove-leading-trailing-only-whitespace ()
+ "Should handle lines with only whitespace."
+ (let ((result (test-remove-leading-trailing " \t ")))
+ (should (string= result ""))))
+
+(ert-deftest test-remove-leading-trailing-no-whitespace ()
+ "Should handle text with no leading/trailing whitespace (no-op)."
+ (let ((result (test-remove-leading-trailing "hello world")))
+ (should (string= result "hello world"))))
+
+(ert-deftest test-remove-leading-trailing-very-long-line ()
+ "Should handle very long lines with whitespace."
+ (let* ((long-text (make-string 500 ?x))
+ (input (concat " " long-text " "))
+ (result (test-remove-leading-trailing input)))
+ (should (string= result long-text))))
+
+(ert-deftest test-remove-leading-trailing-whitespace-between-lines ()
+ "Should handle lines that become empty after removal."
+ (let ((result (test-remove-leading-trailing "line1\n \nline2")))
+ (should (string= result "line1\n\nline2"))))
+
+(ert-deftest test-remove-leading-trailing-newlines-only ()
+ "Should preserve newlines while removing spaces."
+ (let ((result (test-remove-leading-trailing "\n\n\n")))
+ (should (string= result "\n\n\n"))))
+
+(ert-deftest test-remove-leading-trailing-partial-region ()
+ "Should work on partial buffer region."
+ (with-temp-buffer
+ (insert " hello \n world \n test ")
+ ;; Only operate on middle line
+ (let ((start (+ (point-min) 10)) ; Start of second line
+ (end (+ (point-min) 19))) ; End of second line
+ (cj/--remove-leading-trailing-whitespace start end)
+ (should (string= (buffer-string) " hello \nworld\n test ")))))
+
+;;; Error Cases
+
+(ert-deftest test-remove-leading-trailing-start-greater-than-end ()
+ "Should error when start > end."
+ (should-error
+ (with-temp-buffer
+ (insert "hello world")
+ (cj/--remove-leading-trailing-whitespace (point-max) (point-min)))
+ :type 'error))
+
+(ert-deftest test-remove-leading-trailing-empty-region ()
+ "Should handle empty region (start == end) without error."
+ (with-temp-buffer
+ (insert "hello world")
+ (let ((pos (/ (+ (point-min) (point-max)) 2)))
+ (cj/--remove-leading-trailing-whitespace pos pos)
+ ;; Should complete without error and not change buffer
+ (should (string= (buffer-string) "hello world")))))
+
+(provide 'test-custom-whitespace-remove-leading-trailing)
+;;; test-custom-whitespace-remove-leading-trailing.el ends here
diff --git a/tests/test-fixup-whitespace.el.disabled b/tests/test-fixup-whitespace.el.disabled
deleted file mode 100644
index 0126801a..00000000
--- a/tests/test-fixup-whitespace.el.disabled
+++ /dev/null
@@ -1,159 +0,0 @@
-;;; test-fixup-whitespace.el --- -*- lexical-binding: t; -*-
-
-;;; Commentary:
-;; Test cj/fixup-whitespace-line-or-region in custom-functions.el
-
-;; The function under test should:
-;; - ensure there is exactly one space between words
-;; - remove tab characters
-;; - remove leading and trailing whitespace
-;; - operate on a line, or a region, if selected
-
-;;; Code:
-
-
-(require 'ert)
-(add-to-list 'load-path (concat user-emacs-directory "modules"))
-(require 'custom-functions)
-
-(ert-deftest test-cj/fixup-whitespace-positive-first-line-only ()
- "Test a positive case with two lines.
-Both lines have whitespace at the beginning and the end. This tests that when
-this function is called on the first line, only that line is affected."
- (let ((testdata " Hello, world! \n Foo bar ")
- (expected "Hello, world!\n Foo bar ")
- (actual))
- (with-temp-buffer
- (insert testdata)
- (goto-char (point-min))
- (cj/fixup-whitespace-line-or-region)
- (setq actual (buffer-string))
- (should (string= actual expected)))))
-
-(ert-deftest test-cj/fixup-whitespace-positive-first-line-only-tabs ()
- "Test a positive case with two lines.
-Both lines have extraneous whitespace at the beginning and the end, includuing
-tabs. This tests that when this function is called on the first line, only that
-line is affected."
- (let ((testdata " Hello,\t world! \n Foo\tbar ")
- (expected "Hello, world!\n Foo\tbar ")
- (actual))
- (with-temp-buffer
- (insert testdata)
- (goto-char (point-min))
- (cj/fixup-whitespace-line-or-region)
- (setq actual (buffer-string))
- (should (string= actual expected)))))
-
-(ert-deftest test-cj/fixup-whitespace-positive-first-line-only-tabs2 ()
- "Test a positive case with two lines.
-Both lines have extraneous whitespace at the beginning and the end, includuing
-tabs. This tests that when this function is called on the first line, only that
-line is affected."
- (let ((testdata "\t Hello,\tworld! \n Foo\t bar\t ")
- (expected "Hello, world!\n Foo\t bar\t ")
- (actual))
- (with-temp-buffer
- (insert testdata)
- (goto-char (point-min))
- (cj/fixup-whitespace-line-or-region)
- (setq actual (buffer-string))
- (should (string= actual expected)))))
-
-(ert-deftest test-cj/fixup-whitespace-negative-first-line-only ()
- "Test a negative case with two lines.
-Only the second line has whitespace at the beginning and the end. This tests
-that when this function is called on the first line, neither line changes."
- (let ((testdata "Hello, world!\n Foo bar ")
- (expected "Hello, world!\n Foo bar ")
- (actual))
- (with-temp-buffer
- (insert testdata)
- (goto-char (point-min))
- (cj/fixup-whitespace-line-or-region)
- (setq actual (buffer-string))
- (should (string= actual expected)))))
-
-(ert-deftest test-cj/fixup-whitespace-positive-second-line-only ()
- "Test a positive case with two lines.
-Both lines have whitespace at the beginning and the end. This tests that when
-function is called on the second line, only that line is affected."
- (let ((testdata " Hello, world! \n Foo bar ")
- (expected " Hello, world! \nFoo bar")
- (actual))
- (with-temp-buffer
- (insert testdata)
- (goto-char (point-min))
- (forward-line)
- (cj/fixup-whitespace-line-or-region)
- (setq actual (buffer-string))
- (should (string= actual expected)))))
-
-(ert-deftest test-cj/fixup-whitespace-negative-second-line-only ()
- "Test a negative case with two lines.
-Only the first line has whitespace at the beginning and the end. This tests
-that when this function is called on the first line, neither line changes."
- (let ((testdata " Hello, world! \nFoo bar")
- (expected " Hello, world! \nFoo bar")
- (actual))
- (with-temp-buffer
- (insert testdata)
- (goto-char (point-min))
- (forward-line)
- (cj/fixup-whitespace-line-or-region)
- (setq actual (buffer-string))
- (should (string= actual expected)))))
-
-(ert-deftest test-cj/fixup-whitespace-positive-region ()
- "Test a positive case with a region.
-Two lines have whitespace at the beginning, the middle, and the end. This tests
-that when this function is called with a region, all whitespace is cleaned up as
-expected."
- (let ((testdata " Hello, world! \n Foo bar ")
- (expected "Hello, world!\nFoo bar")
- (actual))
- (with-temp-buffer
- (insert testdata)
- (goto-char (point-min))
- (set-mark (point))
- (goto-char (point-max))
- (cj/fixup-whitespace-line-or-region t)
- (setq actual (buffer-string))
- (should (string= actual expected)))))
-
-(ert-deftest test-cj/fixup-whitespace-positive-region-tabs ()
- "Test a positive case with a region and tabs.
-Two lines have extraneous whitespace at the beginning, the middle, and the end.
-This tests that when this function is called with a region, all whitespace is
-cleaned up as expected."
- (let ((testdata " \t \t Hello, world! \n Foo\t bar ")
- (expected "Hello, world!\nFoo bar")
- (actual))
- (with-temp-buffer
- (insert testdata)
- (goto-char (point-min))
- (set-mark (point))
- (goto-char (point-max))
- (cj/fixup-whitespace-line-or-region t)
- (setq actual (buffer-string))
- (should (string= actual expected)))))
-
-(ert-deftest test-cj/fixup-whitespace-negative-region ()
- "Test a negative case with a region.
-Two lines are inserted, neither of which have extraneous whitespace. This tests
-that when this function is called with a region, there's no unwanted
-side-effects and nothing changes."
- (let ((testdata "Hello, world!\nFoo bar")
- (expected "Hello, world!\nFoo bar")
- (actual))
- (with-temp-buffer
- (insert testdata)
- (goto-char (point-min))
- (set-mark (point))
- (goto-char (point-max))
- (cj/fixup-whitespace-line-or-region t)
- (setq actual (buffer-string))
- (should (string= actual expected)))))
-
-(provide 'test-fixup-whitespace)
-;;; test-fixup-whitespace.el ends here.
diff --git a/tests/test-flyspell-config-functions.el.disabled b/tests/test-flyspell-config-functions.el.disabled
deleted file mode 100644
index d12ac167..00000000
--- a/tests/test-flyspell-config-functions.el.disabled
+++ /dev/null
@@ -1,149 +0,0 @@
-;;; test-flyspell-config-functions.el --- -*- lexical-binding: t; -*-
-
-;;; Commentary:
-;; Evaluate the buffer, then run (ert-all-tests).
-
-;;; Code:
-
-(add-to-list 'load-path (concat user-emacs-directory "modules"))
-(require 'flyspell-and-abbrev)
-
-;; --------------------------- Flyspell Overlay Tests --------------------------
-
-(ert-deftest cj/flyspell-overlay-test-positive ()
- "Simplest positive test for \='cj/find-previous-flyspell-overlay\='.
-With one misspelling, cj/find-previous-flyspell-overlay should return the
-character position at the beginning of the misspelled word."
- (with-temp-buffer
- (let ((misspelled "mispeled")
- (overlay-pos))
- ;; insert some text
- (insert (format "some text for testing. %s" misspelled))
-
- ;; trigger flyspell and wait for it to complete
- (flyspell-buffer)
- (sit-for 1)
-
- ;; call the function with position at end of the buffer
- (setq overlay-pos (cj/find-previous-flyspell-overlay (point-max)))
-
- ;; test flyspell-auto-correct-previous-pos is at char position of 'mispeled'.
- (should (eq (- (point-max) (length misspelled)) overlay-pos)))))
-
-(ert-deftest cj/flyspell-overlay-test-negative ()
- "Simplest negative test for \='cj/find-previous-flyspell-overlay\='.
-With no misspelled words, cj/find-previous-flyspell-overlay should return nil."
- (with-temp-buffer
- (insert "This is a correctly spelled sentence.")
- (flyspell-buffer)
- ;; No overlay should exist, so test the result is nil.
- (should-not (cj/find-previous-flyspell-overlay (point-max)))))
-
-(ert-deftest cj/flyspell-overlay-test-positive-multiple ()
- "Positive test for \='cj/find-previous-flyspell-overlay\='.
-With several misspellings above and below, cj/find-previous-flyspell-overlay
-should return the character position at the beginning of the previous misspelled
-word."
- (with-temp-buffer
- (let ((misspelled0 "incorect")
- (misspelled1 "wrongg")
- (misspelled2 "erroor")
- (misspelled3 "mistken")
- (actual-pos)
- (expected-pos)
- (between-pos))
-
- ;; insert some text with misspellings
- (insert (format "flyspell should catch this: %s" misspelled0))
- (insert (format "flyspell should catch this: %s" misspelled1))
-
- ;; calculate the overlay's expected position based on our current position
- (setq expected-pos (- (point) (length misspelled1)))
-
- ;; calculate a position in between misspellings
- (setq between-pos (+ expected-pos (length misspelled1) 5))
-
- ;; insert the rest of the misspellings
- (insert (format "flyspell should catch this: %s" misspelled2))
- (insert (format "flyspell should catch this: %s" misspelled3))
-
- ;; trigger Flyspell and wait for it to identify all misspellings.
- (flyspell-buffer)
- (sit-for 1)
-
- ;; call the function with position in between misspellings
- (setq actual-pos (cj/find-previous-flyspell-overlay between-pos))
- (should (eq expected-pos actual-pos)))))
-
-
-(ert-deftest cj/flyspell-goto-previous-misspelling-positive ()
- "Positive test for \='cj/flyspell-goto-previous-misspelling\='.
-With a simple misspelling above, cj/flyspell-goto-previous-misspelling
-should land on the next misspelled word."
- (with-temp-buffer
- (let ((misspelled-word "incorect")
- (actual-word))
-
- ;; insert some text with misspellings
- (insert (format "flyspell should catch this: %s" misspelled-word))
-
- ;; trigger Flyspell and wait for it to identify all misspellings.
- (flyspell-buffer)
- (sit-for 1)
-
- ;; call the function with position in between misspellings
- (setq actual-word (cj/flyspell-goto-previous-misspelling (point-max)))
- (should (string= misspelled-word actual-word)))))
-
-(ert-deftest cj/flyspell-goto-previous-misspelling-negative ()
- "Negative test for \='cj/flyspell-goto-previous-misspelling\='.
-With no misspellings, cj/flyspell-goto-previous-misspelling return nil."
- (with-temp-buffer
- (let ((expected nil)
- (result))
-
- ;; insert some text with misspellings
- (insert (format "None of these words are misspelled."))
-
- ;; trigger Flyspell and wait for it to identify all misspellings.
- (flyspell-buffer)
- (sit-for 1)
-
- ;; call the function with position in between misspellings
- (setq result (cj/flyspell-goto-previous-misspelling (point-max)))
- (message "result is %s" result)
- (should (eq result expected)))))
-
-(ert-deftest cj/flyspell-goto-previous-misspelling-positive-multiple ()
- "Positive test for \='cj/flyspell-goto-previous-misspelling\='.
-With several misspellings above and below, cj/flyspell-goto-previous-misspelling
-should return the misspelled word just previous to the position of the cursor."
- (with-temp-buffer
- (let ((misspelled0 "incorect")
- (misspelled1 "wrongg")
- (misspelled2 "erroor")
- (misspelled3 "mistken")
- (result)
- (between-pos))
-
- ;; insert some text with misspellings
- (insert (format "flyspell should catch this: %s\n" misspelled0))
- (insert (format "flyspell should catch this: %s\n" misspelled1))
-
- ;; calculate a position in between misspellings
- (setq between-pos (+ (point) (length misspelled1) 5))
-
- ;; insert the rest of the misspellings
- (insert (format "flyspell should catch this: %s\n" misspelled2))
- (insert (format "flyspell should catch this: %s\n" misspelled3))
-
- ;; trigger Flyspell and wait for it to identify all misspellings.
- (flyspell-buffer)
- (sit-for 1)
-
- ;; call the function with position in between misspellings
- (setq result (cj/flyspell-goto-previous-misspelling between-pos))
- (should (string= result misspelled1)))))
-
-(provide 'test-flyspell-config-functions)
-;;; test-flyspell-config-functions.el ends here.
diff --git a/tests/test-format-region.el.disabled b/tests/test-format-region.el.disabled
deleted file mode 100644
index 25d2e52e..00000000
--- a/tests/test-format-region.el.disabled
+++ /dev/null
@@ -1,110 +0,0 @@
-;;; test-format-region.el --- -*- lexical-binding: t; -*-
-
-;;; Commentary:
-;; Some basic tests for the custom function cj/format-region-or-buffer in
-;; custom-functions.el
-
-;;; Code:
-
-(add-to-list 'load-path (concat user-emacs-directory "modules"))
-(require 'custom-functions)
-
-
-;; ----------------------------------- Tests -----------------------------------
-
-(defvar test-format-rob-text-data
- '((" spaces in front\nspaces behind " .
- "spaces in front\nspaces behind")
- ("\t tabs and spaces in front\ntabs and spaces behind\t " .
- "tabs and spaces in front\ntabs and spaces behind")))
-
-(defvar test-format-rob-elisp-data
- '(("(defun existential ()\n(if (eq (+ 3 4) 7)\n(order)\n(chaos)))" .
- "(defun existential ()\n (if (eq (+ 3 4) 7)\n (order)\n (chaos)))")))
-
-
-(ert-deftest test-format-rob-positive-text-region ()
- "Test cj/format-region-or-buffer on a selected region.
-This tests "
- (dolist (data-pair test-format-rob-text-data)
- (let* ((testdata (car data-pair))
- (expected (cdr data-pair))
- (actual))
- (with-temp-buffer
- (insert testdata)
- (goto-char (point-min))
- (set-mark (point))
- (goto-char (point-max))
- (cj/format-region-or-buffer)
- (setq actual (buffer-string))
- (should (string= actual expected))))))
-
-(ert-deftest test-format-rob-positive-text-buffer ()
- "Test cj/format-region-or-buffer on the entire buffer.
-This is the same as testing the region without setting a region in the temp
-buffer."
- (dolist (data-pair test-format-rob-text-data)
- (let* ((testdata (car data-pair))
- (expected (cdr data-pair))
- (actual))
- (with-temp-buffer
- (insert testdata)
- (cj/format-region-or-buffer)
- (setq actual (buffer-string))
- (should (string= actual expected))))))
-
-(ert-deftest test-format-rob-positive-region-text-multiple-paragraphs ()
- "Test cj/format-region-or-buffer on the entire buffer."
- (dolist (data-pair test-format-rob-text-data)
- (let ((testdata (car data-pair))
- (expected1 (cdr data-pair))
- (expected2 (car data-pair))
- (actual1)
- (actual2))
- (with-temp-buffer
- ;; insert data twice with newline char in between
- (insert testdata)
- (insert"\n")
- (insert testdata)
-
- ;; select the first set of data
- (goto-char (point-min))
- (set-mark (point))
- (forward-line 2)
-
- ;; run format and return to top
- (cj/format-region-or-buffer)
- (message "buffer is:\n'%s'" (buffer-string))
-
- ;; assert the first set is formatted
- (goto-char (point-min))
- (setq actual1 (buffer-substring (point-min) (line-end-position 2)))
- (should (string= actual1 expected1))
-
- ;; assert the second set is unformatted
- (goto-char (point-min))
- (setq actual2 (buffer-substring (line-beginning-position 3) (point-max)))
- (should (string= actual2 expected2))))))
-
-(ert-deftest test-format-rob-positive-elisp-region ()
- "Test cj/format-region-or-buffer on a selected region.
-This tests that emacs-lisp specific formatting is applied."
- (ws-butler-mode nil)
- (dolist (data-pair test-format-rob-elisp-data)
- (let* ((testdata (car data-pair))
- (expected (cdr data-pair))
- (actual))
- (with-temp-buffer
- (emacs-lisp-mode)
- (insert testdata)
- (goto-char (point-min))
- (set-mark (point))
- (goto-char (point-max))
- (message "buffer before:\n'%s'" (buffer-string))
- (cj/format-region-or-buffer)
- (message "buffer after:\n'%s'" (buffer-string))
- (setq actual (buffer-string))
- (should (string= actual expected))))))
-
-(provide 'test-format-region)
-;;; test-format-region.el ends here.
diff --git a/tests/test-jumper.el b/tests/test-jumper.el
new file mode 100644
index 00000000..fa65d3f4
--- /dev/null
+++ b/tests/test-jumper.el
@@ -0,0 +1,352 @@
+;;; test-jumper.el --- Tests for jumper.el -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; Unit tests for jumper.el - location navigation using registers.
+;;
+;; Testing approach:
+;; - Tests focus on internal `jumper--do-*` functions (pure business logic)
+;; - Interactive wrappers are thin UI layers and tested minimally
+;; - Each test is isolated with setup/teardown to reset global state
+;; - Tests verify return values, not user messages
+
+;;; Code:
+
+(require 'ert)
+(require 'testutil-general)
+
+;; Add modules directory to load path
+(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory))
+
+;; Load the module
+(require 'jumper)
+
+;;; Test Utilities
+
+(defvar test-jumper--original-registers nil
+ "Backup of jumper registers before test.")
+
+(defvar test-jumper--original-index nil
+ "Backup of jumper index before test.")
+
+(defun test-jumper-setup ()
+ "Reset jumper state before each test."
+ ;; Backup current state
+ (setq test-jumper--original-registers jumper--registers)
+ (setq test-jumper--original-index jumper--next-index)
+ ;; Reset to clean state
+ (setq jumper--registers (make-vector jumper-max-locations nil))
+ (setq jumper--next-index 0))
+
+(defun test-jumper-teardown ()
+ "Restore jumper state after each test."
+ (setq jumper--registers test-jumper--original-registers)
+ (setq jumper--next-index test-jumper--original-index))
+
+;;; Normal Cases - Store Location
+
+(ert-deftest test-jumper-store-first-location ()
+ "Should store first location and return register character."
+ (test-jumper-setup)
+ (with-temp-buffer
+ (insert "test content")
+ (goto-char (point-min))
+ (let ((result (jumper--do-store-location)))
+ (should (= result ?0))
+ (should (= jumper--next-index 1))))
+ (test-jumper-teardown))
+
+(ert-deftest test-jumper-store-multiple-locations ()
+ "Should store multiple locations in sequence."
+ (test-jumper-setup)
+ (with-temp-buffer
+ (insert "line 1\nline 2\nline 3")
+ (goto-char (point-min))
+ (should (= (jumper--do-store-location) ?0))
+ (forward-line 1)
+ (should (= (jumper--do-store-location) ?1))
+ (forward-line 1)
+ (should (= (jumper--do-store-location) ?2))
+ (should (= jumper--next-index 3)))
+ (test-jumper-teardown))
+
+(ert-deftest test-jumper-store-duplicate-location ()
+ "Should detect and reject duplicate locations."
+ (test-jumper-setup)
+ (with-temp-buffer
+ (insert "test content")
+ (goto-char (point-min))
+ (should (= (jumper--do-store-location) ?0))
+ (should (eq (jumper--do-store-location) 'already-exists))
+ (should (= jumper--next-index 1)))
+ (test-jumper-teardown))
+
+;;; Normal Cases - Jump to Location
+
+(ert-deftest test-jumper-jump-to-stored-location ()
+ "Should jump to a previously stored location."
+ (test-jumper-setup)
+ (with-temp-buffer
+ (insert "line 1\nline 2\nline 3")
+ (goto-char (point-min))
+ (jumper--do-store-location)
+ (goto-char (point-max))
+ (let ((result (jumper--do-jump-to-location 0)))
+ (should (eq result 'jumped))
+ (should (= (point) (point-min)))))
+ (test-jumper-teardown))
+
+(ert-deftest test-jumper-jump-toggle-with-single-location ()
+ "Should toggle between current and stored location."
+ (test-jumper-setup)
+ (with-temp-buffer
+ (insert "line 1\nline 2\nline 3")
+ (goto-char (point-min))
+ (jumper--do-store-location)
+ ;; Move away
+ (goto-char (point-max))
+ ;; Toggle should jump back
+ (let ((result (jumper--do-jump-to-location nil)))
+ (should (eq result 'jumped))
+ (should (= (point) (point-min)))))
+ (test-jumper-teardown))
+
+(ert-deftest test-jumper-jump-already-at-location ()
+ "Should detect when already at the only stored location."
+ (test-jumper-setup)
+ (with-temp-buffer
+ (insert "line 1\nline 2")
+ (goto-char (point-min))
+ (jumper--do-store-location)
+ ;; Try to toggle while at the location
+ (let ((result (jumper--do-jump-to-location nil)))
+ (should (eq result 'already-there))))
+ (test-jumper-teardown))
+
+;;; Normal Cases - Remove Location
+
+(ert-deftest test-jumper-remove-location ()
+ "Should remove a stored location."
+ (test-jumper-setup)
+ (with-temp-buffer
+ (insert "test content")
+ (goto-char (point-min))
+ (jumper--do-store-location)
+ (let ((result (jumper--do-remove-location 0)))
+ (should (eq result t))
+ (should (= jumper--next-index 0))))
+ (test-jumper-teardown))
+
+(ert-deftest test-jumper-remove-reorders-registers ()
+ "Should reorder registers after removal from middle."
+ (test-jumper-setup)
+ (with-temp-buffer
+ (insert "line 1\nline 2\nline 3")
+ (goto-char (point-min))
+ (jumper--do-store-location) ; Register 0
+ (forward-line 1)
+ (jumper--do-store-location) ; Register 1
+ (forward-line 1)
+ (jumper--do-store-location) ; Register 2
+ ;; Remove middle (index 1)
+ (jumper--do-remove-location 1)
+ (should (= jumper--next-index 2))
+ ;; What was at index 2 should now be at index 1
+ (should (= (aref jumper--registers 1) ?2)))
+ (test-jumper-teardown))
+
+;;; Boundary Cases - Store Location
+
+(ert-deftest test-jumper-store-at-capacity ()
+ "Should successfully store location at maximum capacity."
+ (test-jumper-setup)
+ (with-temp-buffer
+ (insert "test content")
+ (goto-char (point-min))
+ ;; Fill to capacity
+ (dotimes (i jumper-max-locations)
+ (forward-char 1)
+ (should (= (jumper--do-store-location) (+ ?0 i))))
+ (should (= jumper--next-index jumper-max-locations)))
+ (test-jumper-teardown))
+
+(ert-deftest test-jumper-store-when-full ()
+ "Should return 'no-space when all registers are full."
+ (test-jumper-setup)
+ (with-temp-buffer
+ (insert "01234567890123456789")
+ (goto-char (point-min))
+ ;; Fill to capacity
+ (dotimes (i jumper-max-locations)
+ (forward-char 1)
+ (jumper--do-store-location))
+ ;; Try to store one more
+ (forward-char 1)
+ (should (eq (jumper--do-store-location) 'no-space))
+ (should (= jumper--next-index jumper-max-locations)))
+ (test-jumper-teardown))
+
+(ert-deftest test-jumper-store-in-different-buffers ()
+ "Should store locations across different buffers."
+ (test-jumper-setup)
+ (with-temp-buffer
+ (insert "buffer 1")
+ (goto-char (point-min))
+ (should (= (jumper--do-store-location) ?0))
+ (with-temp-buffer
+ (insert "buffer 2")
+ (goto-char (point-min))
+ (should (= (jumper--do-store-location) ?1))
+ (should (= jumper--next-index 2))))
+ (test-jumper-teardown))
+
+;;; Boundary Cases - Jump to Location
+
+(ert-deftest test-jumper-jump-with-no-locations ()
+ "Should return 'no-locations when nothing is stored."
+ (test-jumper-setup)
+ (with-temp-buffer
+ (insert "test")
+ (let ((result (jumper--do-jump-to-location 0)))
+ (should (eq result 'no-locations))))
+ (test-jumper-teardown))
+
+(ert-deftest test-jumper-jump-to-first-location ()
+ "Should jump to location at index 0."
+ (test-jumper-setup)
+ (with-temp-buffer
+ (insert "line 1\nline 2")
+ (goto-char (point-min))
+ (jumper--do-store-location)
+ (forward-line 1)
+ (jumper--do-store-location)
+ (goto-char (point-max))
+ (jumper--do-jump-to-location 0)
+ (should (= (point) (point-min))))
+ (test-jumper-teardown))
+
+(ert-deftest test-jumper-jump-to-last-location ()
+ "Should jump to last location (register 'z)."
+ (test-jumper-setup)
+ (with-temp-buffer
+ (insert "line 1\nline 2\nline 3")
+ (goto-char (point-min))
+ (jumper--do-store-location)
+ (let ((line2-pos (line-beginning-position 2)))
+ (goto-char line2-pos)
+ ;; Jump to location 0 (this stores current location in 'z)
+ (jumper--do-jump-to-location 0)
+ (should (= (point) (point-min)))
+ ;; Jump to last location should go back to line 2
+ (let ((result (jumper--do-jump-to-location -1)))
+ (should (eq result 'jumped))
+ (should (= (point) line2-pos)))))
+ (test-jumper-teardown))
+
+(ert-deftest test-jumper-jump-to-max-index ()
+ "Should jump to location at maximum index."
+ (test-jumper-setup)
+ (with-temp-buffer
+ (insert "0123456789012345678")
+ (goto-char (point-min))
+ ;; Store at all positions
+ (dotimes (i jumper-max-locations)
+ (forward-char 1)
+ (jumper--do-store-location))
+ (goto-char (point-min))
+ ;; Jump to last one (index 9, which is at position 10)
+ (jumper--do-jump-to-location (1- jumper-max-locations))
+ (should (= (point) (1+ jumper-max-locations))))
+ (test-jumper-teardown))
+
+;;; Boundary Cases - Remove Location
+
+(ert-deftest test-jumper-remove-first-location ()
+ "Should remove location at index 0."
+ (test-jumper-setup)
+ (with-temp-buffer
+ (insert "line 1\nline 2")
+ (goto-char (point-min))
+ (jumper--do-store-location)
+ (forward-line 1)
+ (jumper--do-store-location)
+ (jumper--do-remove-location 0)
+ (should (= jumper--next-index 1))
+ ;; What was at index 1 should now be at index 0
+ (should (= (aref jumper--registers 0) ?1)))
+ (test-jumper-teardown))
+
+(ert-deftest test-jumper-remove-last-location ()
+ "Should remove location at last index."
+ (test-jumper-setup)
+ (with-temp-buffer
+ (insert "line 1\nline 2\nline 3")
+ (goto-char (point-min))
+ (jumper--do-store-location)
+ (forward-line 1)
+ (jumper--do-store-location)
+ (forward-line 1)
+ (jumper--do-store-location)
+ (jumper--do-remove-location 2)
+ (should (= jumper--next-index 2)))
+ (test-jumper-teardown))
+
+(ert-deftest test-jumper-remove-with-cancel ()
+ "Should return 'cancelled when index is -1."
+ (test-jumper-setup)
+ (with-temp-buffer
+ (insert "test")
+ (goto-char (point-min))
+ (jumper--do-store-location)
+ (let ((result (jumper--do-remove-location -1)))
+ (should (eq result 'cancelled))
+ (should (= jumper--next-index 1))))
+ (test-jumper-teardown))
+
+;;; Error Cases
+
+(ert-deftest test-jumper-remove-when-empty ()
+ "Should return 'no-locations when removing from empty list."
+ (test-jumper-setup)
+ (let ((result (jumper--do-remove-location 0)))
+ (should (eq result 'no-locations)))
+ (test-jumper-teardown))
+
+;;; Helper Function Tests
+
+(ert-deftest test-jumper-location-key-format ()
+ "Should generate unique location keys."
+ (with-temp-buffer
+ (insert "line 1\nline 2")
+ (goto-char (point-min))
+ (let ((key1 (jumper--location-key)))
+ (forward-line 1)
+ (let ((key2 (jumper--location-key)))
+ (should-not (string= key1 key2))
+ ;; Keys should contain buffer name and position info
+ (should (string-match-p ":" key1))
+ (should (string-match-p ":" key2))))))
+
+(ert-deftest test-jumper-register-available-p ()
+ "Should correctly report register availability."
+ (test-jumper-setup)
+ (should (jumper--register-available-p))
+ ;; Fill to capacity
+ (setq jumper--next-index jumper-max-locations)
+ (should-not (jumper--register-available-p))
+ (test-jumper-teardown))
+
+(ert-deftest test-jumper-format-location ()
+ "Should format location for display."
+ (test-jumper-setup)
+ (with-temp-buffer
+ (insert "test line with some content")
+ (goto-char (point-min))
+ (jumper--do-store-location)
+ (let ((formatted (jumper--format-location 0)))
+ (should formatted)
+ (should (string-match-p "\\[0\\]" formatted))
+ (should (string-match-p "test line" formatted))))
+ (test-jumper-teardown))
+
+(provide 'test-jumper)
+;;; test-jumper.el ends here
diff --git a/tests/test-lorem-optimum-benchmark.el b/tests/test-lorem-optimum-benchmark.el
new file mode 100644
index 00000000..d3ca2873
--- /dev/null
+++ b/tests/test-lorem-optimum-benchmark.el
@@ -0,0 +1,227 @@
+;;; test-lorem-optimum-benchmark.el --- Performance tests for lorem-optimum.el -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; Benchmark and performance tests for the Markov chain implementation.
+;;
+;; These tests measure:
+;; - Learning time scaling with input size
+;; - Multiple learning operations (exposes key rebuild overhead)
+;; - Generation time scaling
+;; - Memory usage (hash table growth)
+;;
+;; Performance baseline targets (on modern hardware):
+;; - Learn 1000 words: < 10ms
+;; - Learn 10,000 words: < 100ms
+;; - 100 learn operations of 100 words each: < 500ms (current bottleneck!)
+;; - Generate 100 words: < 5ms
+
+;;; Code:
+
+(require 'ert)
+(require 'testutil-general)
+
+;; Add modules directory to load path
+(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory))
+
+;; Load the module
+(require 'lorem-optimum)
+
+;;; Benchmark Helpers
+
+(defun benchmark-time (func)
+ "Time execution of FUNC and return milliseconds."
+ (let ((start (current-time)))
+ (funcall func)
+ (let ((end (current-time)))
+ (* 1000.0 (float-time (time-subtract end start))))))
+
+(defun generate-test-text (word-count)
+ "Generate WORD-COUNT words of test text with some repetition."
+ (let ((words '("lorem" "ipsum" "dolor" "sit" "amet" "consectetur"
+ "adipiscing" "elit" "sed" "do" "eiusmod" "tempor"
+ "incididunt" "ut" "labore" "et" "dolore" "magna" "aliqua"))
+ (result '()))
+ (dotimes (i word-count)
+ (push (nth (mod i (length words)) words) result)
+ (when (zerop (mod i 10))
+ (push "." result)))
+ (mapconcat #'identity (nreverse result) " ")))
+
+(defun benchmark-report (name time-ms)
+ "Report benchmark NAME with TIME-MS."
+ (message "BENCHMARK [%s]: %.2f ms" name time-ms))
+
+;;; Learning Performance Tests
+
+(ert-deftest benchmark-learn-1k-words ()
+ "Benchmark learning 1000 words."
+ (let* ((text (generate-test-text 1000))
+ (chain (cj/markov-chain-create))
+ (time (benchmark-time
+ (lambda () (cj/markov-learn chain text)))))
+ (benchmark-report "Learn 1K words" time)
+ (should (< time 50.0)))) ; Should be < 50ms
+
+(ert-deftest benchmark-learn-10k-words ()
+ "Benchmark learning 10,000 words."
+ (let* ((text (generate-test-text 10000))
+ (chain (cj/markov-chain-create))
+ (time (benchmark-time
+ (lambda () (cj/markov-learn chain text)))))
+ (benchmark-report "Learn 10K words" time)
+ (should (< time 500.0)))) ; Should be < 500ms
+
+(ert-deftest benchmark-learn-100k-words ()
+ "Benchmark learning 100,000 words (stress test)."
+ :tags '(:slow)
+ (let* ((text (generate-test-text 100000))
+ (chain (cj/markov-chain-create))
+ (time (benchmark-time
+ (lambda () (cj/markov-learn chain text)))))
+ (benchmark-report "Learn 100K words" time)
+ ;; This may be slow due to key rebuild
+ (message "Hash table size: %d bigrams"
+ (hash-table-count (cj/markov-chain-map chain)))))
+
+;;; Multiple Learning Operations (Exposes Quadratic Behavior)
+
+(ert-deftest benchmark-multiple-learns-10x100 ()
+ "Benchmark 10 learn operations of 100 words each."
+ (let ((chain (cj/markov-chain-create))
+ (times '()))
+ (dotimes (i 10)
+ (let* ((text (generate-test-text 100))
+ (time (benchmark-time
+ (lambda () (cj/markov-learn chain text)))))
+ (push time times)))
+ (let ((total (apply #'+ times))
+ (avg (/ (apply #'+ times) 10.0))
+ (max-time (apply #'max times)))
+ (benchmark-report "10x learn 100 words - TOTAL" total)
+ (benchmark-report "10x learn 100 words - AVG" avg)
+ (benchmark-report "10x learn 100 words - MAX" max-time)
+ (message "Times: %S" (nreverse times))
+ ;; Note: Watch if later operations are slower (quadratic behavior)
+ (should (< total 100.0))))) ; Total should be < 100ms
+
+(ert-deftest benchmark-multiple-learns-100x100 ()
+ "Benchmark 100 learn operations of 100 words each (key rebuild overhead)."
+ :tags '(:slow)
+ (let ((chain (cj/markov-chain-create))
+ (times '())
+ (measurements '()))
+ (dotimes (i 100)
+ (let* ((text (generate-test-text 100))
+ (time (benchmark-time
+ (lambda () (cj/markov-learn chain text)))))
+ (push time times)
+ ;; Sample measurements every 10 iterations
+ (when (zerop (mod i 10))
+ (push (cons i time) measurements))))
+ (let ((total (apply #'+ times))
+ (avg (/ (apply #'+ times) 100.0))
+ (first-10-avg (/ (apply #'+ (last times 10)) 10.0))
+ (last-10-avg (/ (apply #'+ (seq-take times 10)) 10.0)))
+ (benchmark-report "100x learn 100 words - TOTAL" total)
+ (benchmark-report "100x learn 100 words - AVG" avg)
+ (benchmark-report "100x learn - First 10 AVG" first-10-avg)
+ (benchmark-report "100x learn - Last 10 AVG" last-10-avg)
+ (message "Sampled times (iteration, ms): %S" (nreverse measurements))
+ (message "Hash table size: %d bigrams"
+ (hash-table-count (cj/markov-chain-map chain)))
+ ;; This exposes the quadratic behavior: last operations much slower
+ (when (> last-10-avg (* 2.0 first-10-avg))
+ (message "WARNING: Learning slows down significantly over time!")
+ (message " First 10 avg: %.2f ms" first-10-avg)
+ (message " Last 10 avg: %.2f ms" last-10-avg)
+ (message " Ratio: %.1fx slower" (/ last-10-avg first-10-avg))))))
+
+;;; Generation Performance Tests
+
+(ert-deftest benchmark-generate-100-words ()
+ "Benchmark generating 100 words."
+ (let* ((text (generate-test-text 1000))
+ (chain (cj/markov-chain-create)))
+ (cj/markov-learn chain text)
+ (let ((time (benchmark-time
+ (lambda () (cj/markov-generate chain 100)))))
+ (benchmark-report "Generate 100 words" time)
+ (should (< time 20.0))))) ; Should be < 20ms
+
+(ert-deftest benchmark-generate-1000-words ()
+ "Benchmark generating 1000 words."
+ (let* ((text (generate-test-text 10000))
+ (chain (cj/markov-chain-create)))
+ (cj/markov-learn chain text)
+ (let ((time (benchmark-time
+ (lambda () (cj/markov-generate chain 1000)))))
+ (benchmark-report "Generate 1000 words" time)
+ (should (< time 100.0))))) ; Should be < 100ms
+
+;;; Tokenization Performance Tests
+
+(ert-deftest benchmark-tokenize-10k-words ()
+ "Benchmark tokenizing 10,000 words."
+ (let* ((text (generate-test-text 10000))
+ (time (benchmark-time
+ (lambda () (cj/markov-tokenize text)))))
+ (benchmark-report "Tokenize 10K words" time)
+ (should (< time 50.0)))) ; Tokenization should be fast
+
+;;; Memory/Size Tests
+
+(ert-deftest benchmark-chain-growth ()
+ "Measure hash table growth with increasing input."
+ (let ((chain (cj/markov-chain-create))
+ (sizes '()))
+ (dolist (word-count '(100 500 1000 5000 10000))
+ (let ((text (generate-test-text word-count)))
+ (cj/markov-learn chain text)
+ (let ((size (hash-table-count (cj/markov-chain-map chain))))
+ (push (cons word-count size) sizes)
+ (message "After %d words: %d unique bigrams" word-count size))))
+ (message "Growth pattern: %S" (nreverse sizes))))
+
+;;; Comparison: Tokenization vs Learning
+
+(ert-deftest benchmark-tokenize-vs-learn ()
+ "Compare tokenization time to total learning time."
+ (let* ((text (generate-test-text 5000))
+ (tokenize-time (benchmark-time
+ (lambda () (cj/markov-tokenize text))))
+ (chain (cj/markov-chain-create))
+ (learn-time (benchmark-time
+ (lambda () (cj/markov-learn chain text)))))
+ (benchmark-report "Tokenize 5K words" tokenize-time)
+ (benchmark-report "Learn 5K words (total)" learn-time)
+ (message "Tokenization is %.1f%% of total learning time"
+ (* 100.0 (/ tokenize-time learn-time)))))
+
+;;; Real-world Scenario
+
+(ert-deftest benchmark-realistic-usage ()
+ "Benchmark realistic usage: learn from multiple sources, generate paragraphs."
+ (let ((chain (cj/markov-chain-create))
+ (learn-total 0.0)
+ (gen-total 0.0))
+ ;; Simulate learning from 10 different sources
+ (dotimes (i 10)
+ (let ((text (generate-test-text 500)))
+ (setq learn-total
+ (+ learn-total
+ (benchmark-time (lambda () (cj/markov-learn chain text)))))))
+
+ ;; Generate 5 paragraphs
+ (dotimes (i 5)
+ (setq gen-total
+ (+ gen-total
+ (benchmark-time (lambda () (cj/markov-generate chain 50))))))
+
+ (benchmark-report "Realistic: 10 learns (500 words each)" learn-total)
+ (benchmark-report "Realistic: 5 generations (50 words each)" gen-total)
+ (benchmark-report "Realistic: TOTAL TIME" (+ learn-total gen-total))
+ (message "Final chain size: %d bigrams"
+ (hash-table-count (cj/markov-chain-map chain)))))
+
+(provide 'test-lorem-optimum-benchmark)
+;;; test-lorem-optimum-benchmark.el ends here
diff --git a/tests/test-lorem-optimum.el b/tests/test-lorem-optimum.el
new file mode 100644
index 00000000..ca2e52f4
--- /dev/null
+++ b/tests/test-lorem-optimum.el
@@ -0,0 +1,242 @@
+;;; test-lorem-optimum.el --- Tests for lorem-optimum.el -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; Unit tests for lorem-optimum.el Markov chain text generation.
+;;
+;; Tests cover:
+;; - Tokenization
+;; - Learning and chain building
+;; - Text generation
+;; - Capitalization fixing
+;; - Token joining
+
+;;; Code:
+
+(require 'ert)
+(require 'testutil-general)
+
+;; Add modules directory to load path
+(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory))
+
+;; Load the module
+(require 'lorem-optimum)
+
+;;; Test Helpers
+
+(defun test-chain ()
+ "Create a fresh test chain."
+ (cj/markov-chain-create))
+
+(defun test-learn (text)
+ "Create a chain and learn TEXT."
+ (let ((chain (test-chain)))
+ (cj/markov-learn chain text)
+ chain))
+
+;;; Tokenization Tests
+
+(ert-deftest test-tokenize-simple ()
+ "Should tokenize simple words."
+ (let ((result (cj/markov-tokenize "hello world")))
+ (should (equal result '("hello" "world")))))
+
+(ert-deftest test-tokenize-with-punctuation ()
+ "Should separate punctuation as tokens."
+ (let ((result (cj/markov-tokenize "Hello, world!")))
+ (should (equal result '("Hello" "," "world" "!")))))
+
+(ert-deftest test-tokenize-multiple-spaces ()
+ "Should handle multiple spaces."
+ (let ((result (cj/markov-tokenize "hello world")))
+ (should (equal result '("hello" "world")))))
+
+(ert-deftest test-tokenize-newlines ()
+ "Should handle newlines as whitespace."
+ (let ((result (cj/markov-tokenize "hello\nworld")))
+ (should (equal result '("hello" "world")))))
+
+(ert-deftest test-tokenize-mixed-punctuation ()
+ "Should tokenize complex punctuation."
+ (let ((result (cj/markov-tokenize "one, two; three.")))
+ (should (equal result '("one" "," "two" ";" "three" ".")))))
+
+(ert-deftest test-tokenize-empty ()
+ "Should handle empty string."
+ (let ((result (cj/markov-tokenize "")))
+ (should (null result))))
+
+(ert-deftest test-tokenize-whitespace-only ()
+ "Should return nil for whitespace only."
+ (let ((result (cj/markov-tokenize " \n\t ")))
+ (should (null result))))
+
+;;; Markov Learn Tests
+
+(ert-deftest test-learn-basic ()
+ "Should learn simple text."
+ (let ((chain (test-learn "one two three four")))
+ (should (cj/markov-chain-p chain))
+ (should (> (hash-table-count (cj/markov-chain-map chain)) 0))))
+
+(ert-deftest test-learn-creates-bigrams ()
+ "Should create bigram mappings."
+ (let ((chain (test-learn "one two three")))
+ (should (gethash '("one" "two") (cj/markov-chain-map chain)))))
+
+(ert-deftest test-learn-stores-following-word ()
+ "Should store following word for bigram."
+ (let ((chain (test-learn "one two three")))
+ (should (member "three" (gethash '("one" "two") (cj/markov-chain-map chain))))))
+
+(ert-deftest test-learn-builds-keys-list ()
+ "Should build keys list lazily when accessed."
+ (let ((chain (test-learn "one two three four")))
+ ;; Keys are built lazily, so initially nil
+ (should (null (cj/markov-chain-keys chain)))
+ ;; After calling random-key, keys should be built
+ (cj/markov-random-key chain)
+ (should (> (length (cj/markov-chain-keys chain)) 0))))
+
+(ert-deftest test-learn-repeated-patterns ()
+ "Should accumulate repeated patterns."
+ (let ((chain (test-learn "one two three one two four")))
+ (let ((nexts (gethash '("one" "two") (cj/markov-chain-map chain))))
+ (should (= (length nexts) 2))
+ (should (member "three" nexts))
+ (should (member "four" nexts)))))
+
+(ert-deftest test-learn-incremental ()
+ "Should support incremental learning."
+ (let ((chain (test-chain)))
+ (cj/markov-learn chain "one two three")
+ (cj/markov-learn chain "four five six")
+ (should (> (hash-table-count (cj/markov-chain-map chain)) 0))))
+
+;;; Token Joining Tests
+
+(ert-deftest test-join-simple-words ()
+ "Should join words with spaces."
+ (let ((result (cj/markov-join-tokens '("hello" "world"))))
+ (should (string-match-p "^Hello world" result))))
+
+(ert-deftest test-join-with-punctuation ()
+ "Should attach punctuation without spaces."
+ (let ((result (cj/markov-join-tokens '("hello" "," "world"))))
+ (should (string-match-p "Hello, world" result))))
+
+(ert-deftest test-join-capitalizes-first ()
+ "Should capitalize first word."
+ (let ((result (cj/markov-join-tokens '("hello" "world"))))
+ (should (string-match-p "^H" result))))
+
+(ert-deftest test-join-adds-period ()
+ "Should add period if missing."
+ (let ((result (cj/markov-join-tokens '("hello" "world"))))
+ (should (string-match-p "\\.$" result))))
+
+(ert-deftest test-join-preserves-existing-period ()
+ "Should not double-add period."
+ (let ((result (cj/markov-join-tokens '("hello" "world" "."))))
+ (should (string-match-p "\\.$" result))
+ (should-not (string-match-p "\\.\\.$" result))))
+
+(ert-deftest test-join-empty-tokens ()
+ "Should handle empty token list."
+ (let ((result (cj/markov-join-tokens '())))
+ (should (equal result "."))))
+
+;;; Capitalization Tests
+
+(ert-deftest test-capitalize-first-word ()
+ "Should capitalize first word."
+ (let ((result (cj/markov-fix-capitalization "hello world")))
+ (should (string-match-p "^Hello" result))))
+
+(ert-deftest test-capitalize-after-period ()
+ "Should capitalize after period."
+ (let ((result (cj/markov-fix-capitalization "hello. world")))
+ (should (string-match-p "Hello\\. World" result))))
+
+(ert-deftest test-capitalize-after-exclamation ()
+ "Should capitalize after exclamation."
+ (let ((result (cj/markov-fix-capitalization "hello! world")))
+ (should (string-match-p "Hello! World" result))))
+
+(ert-deftest test-capitalize-after-question ()
+ "Should capitalize after question mark."
+ (let ((result (cj/markov-fix-capitalization "hello? world")))
+ (should (string-match-p "Hello\\? World" result))))
+
+(ert-deftest test-capitalize-skip-non-alpha ()
+ "Should skip non-alphabetic tokens."
+ (let ((result (cj/markov-fix-capitalization "hello. 123 world")))
+ (should (string-match-p "123" result))))
+
+(ert-deftest test-capitalize-multiple-sentences ()
+ "Should capitalize all sentences."
+ (let ((result (cj/markov-fix-capitalization "first. second. third")))
+ (should (string-match-p "First\\. Second\\. Third" result))))
+
+;;; Generation Tests (deterministic with fixed chain)
+
+(ert-deftest test-generate-produces-output ()
+ "Should generate non-empty output."
+ (let ((chain (test-learn "Lorem ipsum dolor sit amet consectetur adipiscing elit")))
+ (let ((result (cj/markov-generate chain 5)))
+ (should (stringp result))
+ (should (> (length result) 0)))))
+
+(ert-deftest test-generate-empty-chain ()
+ "Should handle empty chain gracefully."
+ (let ((chain (test-chain)))
+ (let ((result (cj/markov-generate chain 5)))
+ (should (or (null result) (string-empty-p result))))))
+
+(ert-deftest test-generate-respects-start ()
+ "Should use provided start state if available."
+ (let ((chain (test-learn "Lorem ipsum dolor sit amet")))
+ (let ((result (cj/markov-generate chain 3 '("Lorem" "ipsum"))))
+ (should (stringp result))
+ ;; Should start with Lorem or similar
+ (should (> (length result) 0)))))
+
+;;; Integration Tests
+
+(ert-deftest test-full-workflow ()
+ "Should complete full learn-generate workflow."
+ (let ((chain (test-chain)))
+ (cj/markov-learn chain "The quick brown fox jumps over the lazy dog")
+ (let ((result (cj/markov-generate chain 8)))
+ (should (stringp result))
+ (should (> (length result) 0))
+ (should (string-match-p "^[A-Z]" result))
+ (should (string-match-p "[.!?]$" result)))))
+
+(ert-deftest test-latin-like-output ()
+ "Should generate Latin-like text from Latin input."
+ (let ((chain (test-chain)))
+ (cj/markov-learn chain "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.")
+ (let ((result (cj/markov-generate chain 10)))
+ (should (stringp result))
+ (should (> (length result) 10)))))
+
+;;; Edge Cases
+
+(ert-deftest test-learn-short-text ()
+ "Should handle text shorter than trigram."
+ (let ((chain (test-learn "one two")))
+ (should (cj/markov-chain-p chain))))
+
+(ert-deftest test-learn-single-word ()
+ "Should handle single word."
+ (let ((chain (test-learn "word")))
+ (should (cj/markov-chain-p chain))))
+
+(ert-deftest test-generate-requested-count-small ()
+ "Should handle small generation count."
+ (let ((chain (test-learn "one two three four five")))
+ (let ((result (cj/markov-generate chain 2)))
+ (should (stringp result)))))
+
+(provide 'test-lorem-optimum)
+;;; test-lorem-optimum.el ends here
diff --git a/tests/test-org-contacts-parse-email.el b/tests/test-org-contacts-parse-email.el
new file mode 100644
index 00000000..37e79fba
--- /dev/null
+++ b/tests/test-org-contacts-parse-email.el
@@ -0,0 +1,219 @@
+;;; test-org-contacts-parse-email.el --- Tests for cj/--parse-email-string -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; Tests for the cj/--parse-email-string function from org-contacts-config.el
+;;
+;; This function parses a string containing one or more email addresses
+;; separated by commas, semicolons, or spaces, and formats them as
+;; "Name <email>" strings.
+;;
+;; Examples:
+;; Input: name="John Doe", email-string="john@example.com"
+;; Output: '("John Doe <john@example.com>")
+;;
+;; Input: name="Jane Smith", email-string="jane@work.com, jane@home.com"
+;; Output: '("Jane Smith <jane@work.com>" "Jane Smith <jane@home.com>")
+
+;;; Code:
+
+(require 'ert)
+(require 'testutil-general)
+
+;; Add modules directory to load path
+(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory))
+
+;; Now load the actual production module
+(require 'org-contacts-config)
+
+;;; Test Helpers
+
+(defun test-parse-email (name email-string)
+ "Test cj/--parse-email-string with NAME and EMAIL-STRING.
+Returns the formatted email list."
+ (cj/--parse-email-string name email-string))
+
+;;; Normal Cases - Single Email
+
+(ert-deftest test-parse-single-email ()
+ "Should format single email address."
+ (let ((result (test-parse-email "John Doe" "john@example.com")))
+ (should (equal result '("John Doe <john@example.com>")))))
+
+(ert-deftest test-parse-single-email-with-subdomain ()
+ "Should handle email with subdomain."
+ (let ((result (test-parse-email "Jane Smith" "jane@mail.company.com")))
+ (should (equal result '("Jane Smith <jane@mail.company.com>")))))
+
+(ert-deftest test-parse-email-with-numbers ()
+ "Should handle email containing numbers."
+ (let ((result (test-parse-email "User 123" "user123@test.com")))
+ (should (equal result '("User 123 <user123@test.com>")))))
+
+(ert-deftest test-parse-email-with-dots ()
+ "Should handle email with dots in local part."
+ (let ((result (test-parse-email "Bob Jones" "bob.jones@example.com")))
+ (should (equal result '("Bob Jones <bob.jones@example.com>")))))
+
+(ert-deftest test-parse-email-with-hyphen ()
+ "Should handle email with hyphens."
+ (let ((result (test-parse-email "Alice Brown" "alice-brown@test-domain.com")))
+ (should (equal result '("Alice Brown <alice-brown@test-domain.com>")))))
+
+;;; Normal Cases - Multiple Emails with Different Separators
+
+(ert-deftest test-parse-two-emails-comma ()
+ "Should parse two emails separated by comma."
+ (let ((result (test-parse-email "John Doe" "john@work.com, john@home.com")))
+ (should (equal result '("John Doe <john@work.com>" "John Doe <john@home.com>")))))
+
+(ert-deftest test-parse-two-emails-semicolon ()
+ "Should parse two emails separated by semicolon."
+ (let ((result (test-parse-email "Jane Smith" "jane@work.com; jane@home.com")))
+ (should (equal result '("Jane Smith <jane@work.com>" "Jane Smith <jane@home.com>")))))
+
+(ert-deftest test-parse-two-emails-space ()
+ "Should parse two emails separated by space."
+ (let ((result (test-parse-email "Bob Jones" "bob@work.com bob@home.com")))
+ (should (equal result '("Bob Jones <bob@work.com>" "Bob Jones <bob@home.com>")))))
+
+(ert-deftest test-parse-three-emails-mixed-separators ()
+ "Should parse emails with mixed separators."
+ (let ((result (test-parse-email "Alice" "alice@a.com, alice@b.com; alice@c.com")))
+ (should (equal result '("Alice <alice@a.com>" "Alice <alice@b.com>" "Alice <alice@c.com>")))))
+
+(ert-deftest test-parse-multiple-emails-with-spaces ()
+ "Should parse comma-separated emails with spaces."
+ (let ((result (test-parse-email "User" "a@test.com , b@test.com , c@test.com")))
+ (should (equal result '("User <a@test.com>" "User <b@test.com>" "User <c@test.com>")))))
+
+;;; Normal Cases - Whitespace Handling
+
+(ert-deftest test-parse-email-leading-whitespace ()
+ "Should trim leading whitespace from email."
+ (let ((result (test-parse-email "John" " john@example.com")))
+ (should (equal result '("John <john@example.com>")))))
+
+(ert-deftest test-parse-email-trailing-whitespace ()
+ "Should trim trailing whitespace from email."
+ (let ((result (test-parse-email "Jane" "jane@example.com ")))
+ (should (equal result '("Jane <jane@example.com>")))))
+
+(ert-deftest test-parse-email-surrounding-whitespace ()
+ "Should trim surrounding whitespace from email."
+ (let ((result (test-parse-email "Bob" " bob@example.com ")))
+ (should (equal result '("Bob <bob@example.com>")))))
+
+(ert-deftest test-parse-emails-with-tabs ()
+ "Should handle emails separated by tabs."
+ (let ((result (test-parse-email "User" "a@test.com\tb@test.com")))
+ (should (equal result '("User <a@test.com>" "User <b@test.com>")))))
+
+;;; Edge Cases - Empty and Nil
+
+(ert-deftest test-parse-nil-email-string ()
+ "Should return nil for nil email string."
+ (let ((result (test-parse-email "John Doe" nil)))
+ (should (null result))))
+
+(ert-deftest test-parse-empty-email-string ()
+ "Should return nil for empty email string."
+ (let ((result (test-parse-email "Jane Smith" "")))
+ (should (null result))))
+
+(ert-deftest test-parse-whitespace-only ()
+ "Should return nil for whitespace-only string."
+ (let ((result (test-parse-email "Bob Jones" " ")))
+ (should (null result))))
+
+(ert-deftest test-parse-tabs-only ()
+ "Should return nil for tabs-only string."
+ (let ((result (test-parse-email "Alice" "\t\t\t")))
+ (should (null result))))
+
+(ert-deftest test-parse-mixed-whitespace-only ()
+ "Should return nil for mixed whitespace."
+ (let ((result (test-parse-email "User" " \t \n ")))
+ (should (null result))))
+
+;;; Edge Cases - Multiple Consecutive Separators
+
+(ert-deftest test-parse-multiple-commas ()
+ "Should handle multiple consecutive commas."
+ (let ((result (test-parse-email "John" "john@a.com,,,john@b.com")))
+ (should (equal result '("John <john@a.com>" "John <john@b.com>")))))
+
+(ert-deftest test-parse-multiple-semicolons ()
+ "Should handle multiple consecutive semicolons."
+ (let ((result (test-parse-email "Jane" "jane@a.com;;;jane@b.com")))
+ (should (equal result '("Jane <jane@a.com>" "Jane <jane@b.com>")))))
+
+(ert-deftest test-parse-multiple-spaces ()
+ "Should handle multiple consecutive spaces."
+ (let ((result (test-parse-email "Bob" "bob@a.com bob@b.com")))
+ (should (equal result '("Bob <bob@a.com>" "Bob <bob@b.com>")))))
+
+(ert-deftest test-parse-mixed-multiple-separators ()
+ "Should handle mixed consecutive separators."
+ (let ((result (test-parse-email "User" "a@test.com , ; b@test.com")))
+ (should (equal result '("User <a@test.com>" "User <b@test.com>")))))
+
+;;; Edge Cases - Special Name Formats
+
+(ert-deftest test-parse-name-with-title ()
+ "Should handle name with title."
+ (let ((result (test-parse-email "Dr. John Smith" "john@example.com")))
+ (should (equal result '("Dr. John Smith <john@example.com>")))))
+
+(ert-deftest test-parse-name-with-suffix ()
+ "Should handle name with suffix."
+ (let ((result (test-parse-email "John Doe Jr." "john@example.com")))
+ (should (equal result '("John Doe Jr. <john@example.com>")))))
+
+(ert-deftest test-parse-name-with-special-chars ()
+ "Should handle name with special characters."
+ (let ((result (test-parse-email "O'Brien, Patrick" "patrick@example.com")))
+ (should (equal result '("O'Brien, Patrick <patrick@example.com>")))))
+
+(ert-deftest test-parse-unicode-name ()
+ "Should handle Unicode characters in name."
+ (let ((result (test-parse-email "José García" "jose@example.com")))
+ (should (equal result '("José García <jose@example.com>")))))
+
+;;; Edge Cases - Special Email Formats
+
+(ert-deftest test-parse-email-with-plus ()
+ "Should handle email with plus sign."
+ (let ((result (test-parse-email "User" "user+tag@example.com")))
+ (should (equal result '("User <user+tag@example.com>")))))
+
+(ert-deftest test-parse-email-with-underscore ()
+ "Should handle email with underscore."
+ (let ((result (test-parse-email "User" "user_name@example.com")))
+ (should (equal result '("User <user_name@example.com>")))))
+
+(ert-deftest test-parse-very-long-email ()
+ "Should handle very long email address."
+ (let* ((long-local (make-string 50 ?a))
+ (email (concat long-local "@example.com"))
+ (result (test-parse-email "User" email)))
+ (should (equal result (list (format "User <%s>" email))))))
+
+;;; Integration Tests
+
+(ert-deftest test-parse-realistic-contact ()
+ "Should parse realistic contact with multiple emails."
+ (let ((result (test-parse-email "John Doe" "john.doe@company.com, jdoe@personal.com")))
+ (should (equal result '("John Doe <john.doe@company.com>" "John Doe <jdoe@personal.com>")))))
+
+(ert-deftest test-parse-messy-input ()
+ "Should handle messy real-world input."
+ (let ((result (test-parse-email "Jane Smith" " jane@work.com ; jane@home.com,jane@mobile.com ")))
+ (should (equal result '("Jane Smith <jane@work.com>" "Jane Smith <jane@home.com>" "Jane Smith <jane@mobile.com>")))))
+
+(ert-deftest test-parse-single-with-extra-separators ()
+ "Should handle single email with trailing separators."
+ (let ((result (test-parse-email "Bob" "bob@example.com;;;")))
+ (should (equal result '("Bob <bob@example.com>")))))
+
+(provide 'test-org-contacts-parse-email)
+;;; test-org-contacts-parse-email.el ends here
diff --git a/tests/test-org-roam-config-demote.el b/tests/test-org-roam-config-demote.el
new file mode 100644
index 00000000..98cc8244
--- /dev/null
+++ b/tests/test-org-roam-config-demote.el
@@ -0,0 +1,183 @@
+;;; test-org-roam-config-demote.el --- Tests for cj/--demote-org-subtree -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; Tests for the cj/--demote-org-subtree function from org-roam-config.el
+;;
+;; This function demotes org subtree content from one level to another.
+;; All headings in the tree are adjusted proportionally, with a minimum level of 1.
+;;
+;; Examples:
+;; Input: "*** Heading\n**** Sub", from: 3, to: 1
+;; Output: "* Heading\n** Sub"
+;;
+;; Input: "** Heading\n*** Sub", from: 2, to: 1
+;; Output: "* Heading\n** Sub"
+
+;;; Code:
+
+(require 'ert)
+(require 'testutil-general)
+
+;; Add modules directory to load path
+(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory))
+
+;; Now load the actual production module
+(require 'org-roam-config)
+
+;;; Test Helpers
+
+(defun test-demote (content from-level to-level)
+ "Test cj/--demote-org-subtree on CONTENT.
+FROM-LEVEL is the current top level, TO-LEVEL is the desired top level.
+Returns the demoted content."
+ (cj/--demote-org-subtree content from-level to-level))
+
+;;; Normal Cases - Single Heading
+
+(ert-deftest test-demote-level2-to-level1 ()
+ "Should demote level 2 heading to level 1."
+ (let ((result (test-demote "** Heading\n" 2 1)))
+ (should (string= result "* Heading\n"))))
+
+(ert-deftest test-demote-level3-to-level1 ()
+ "Should demote level 3 heading to level 1."
+ (let ((result (test-demote "*** Heading\n" 3 1)))
+ (should (string= result "* Heading\n"))))
+
+(ert-deftest test-demote-level4-to-level1 ()
+ "Should demote level 4 heading to level 1."
+ (let ((result (test-demote "**** Heading\n" 4 1)))
+ (should (string= result "* Heading\n"))))
+
+(ert-deftest test-demote-level3-to-level2 ()
+ "Should demote level 3 heading to level 2."
+ (let ((result (test-demote "*** Heading\n" 3 2)))
+ (should (string= result "** Heading\n"))))
+
+;;; Normal Cases - Multiple Headings at Same Level
+
+(ert-deftest test-demote-multiple-same-level ()
+ "Should demote multiple headings at same level."
+ (let ((result (test-demote "** First\n** Second\n** Third\n" 2 1)))
+ (should (string= result "* First\n* Second\n* Third\n"))))
+
+;;; Normal Cases - Hierarchical Structure
+
+(ert-deftest test-demote-with-subheading ()
+ "Should demote heading and subheading proportionally."
+ (let ((result (test-demote "** Heading\n*** Subheading\n" 2 1)))
+ (should (string= result "* Heading\n** Subheading\n"))))
+
+(ert-deftest test-demote-three-levels ()
+ "Should demote three-level hierarchy."
+ (let ((result (test-demote "** Main\n*** Sub\n**** SubSub\n" 2 1)))
+ (should (string= result "* Main\n** Sub\n*** SubSub\n"))))
+
+(ert-deftest test-demote-complex-hierarchy ()
+ "Should demote complex hierarchy maintaining relative structure."
+ (let ((result (test-demote "*** Top\n**** Sub1\n***** Deep\n**** Sub2\n" 3 1)))
+ (should (string= result "* Top\n** Sub1\n*** Deep\n** Sub2\n"))))
+
+;;; Normal Cases - With Content
+
+(ert-deftest test-demote-heading-with-text ()
+ "Should demote heading preserving body text."
+ (let ((result (test-demote "** Heading\nBody text\n" 2 1)))
+ (should (string= result "* Heading\nBody text\n"))))
+
+(ert-deftest test-demote-with-properties ()
+ "Should demote heading preserving properties."
+ (let ((result (test-demote "** Heading\n:PROPERTIES:\n:ID: 123\n:END:\n" 2 1)))
+ (should (string= result "* Heading\n:PROPERTIES:\n:ID: 123\n:END:\n"))))
+
+(ert-deftest test-demote-with-mixed-content ()
+ "Should demote headings preserving all content."
+ (let ((result (test-demote "** H1\nText\n*** H2\nMore text\n" 2 1)))
+ (should (string= result "* H1\nText\n** H2\nMore text\n"))))
+
+;;; Boundary Cases - No Demotion Needed
+
+(ert-deftest test-demote-same-level ()
+ "Should return content unchanged when from equals to."
+ (let ((result (test-demote "* Heading\n" 1 1)))
+ (should (string= result "* Heading\n"))))
+
+(ert-deftest test-demote-promote-ignored ()
+ "Should return content unchanged when to > from (promotion)."
+ (let ((result (test-demote "* Heading\n" 1 2)))
+ (should (string= result "* Heading\n"))))
+
+;;; Boundary Cases - Minimum Level
+
+(ert-deftest test-demote-respects-minimum-level ()
+ "Should not demote below level 1."
+ (let ((result (test-demote "** Main\n*** Sub\n" 2 1)))
+ (should (string= result "* Main\n** Sub\n"))
+ ;; Sub went from 3 to 2, not below 1
+ (should (string-match-p "^\\*\\* Sub" result))))
+
+(ert-deftest test-demote-deep-hierarchy-min-level ()
+ "Should respect minimum level for deep hierarchies."
+ (let ((result (test-demote "**** L4\n***** L5\n****** L6\n" 4 1)))
+ (should (string= result "* L4\n** L5\n*** L6\n"))))
+
+;;; Boundary Cases - Empty and Edge Cases
+
+(ert-deftest test-demote-empty-string ()
+ "Should handle empty string."
+ (let ((result (test-demote "" 2 1)))
+ (should (string= result ""))))
+
+(ert-deftest test-demote-no-headings ()
+ "Should return non-heading content unchanged."
+ (let ((result (test-demote "Just plain text\nNo headings here\n" 2 1)))
+ (should (string= result "Just plain text\nNo headings here\n"))))
+
+(ert-deftest test-demote-heading-without-space ()
+ "Should not match headings without space after stars."
+ (let ((result (test-demote "**Not a heading\n** Real Heading\n" 2 1)))
+ (should (string= result "**Not a heading\n* Real Heading\n"))))
+
+;;; Edge Cases - Special Heading Content
+
+(ert-deftest test-demote-heading-with-tags ()
+ "Should demote heading preserving tags."
+ (let ((result (test-demote "** Heading :tag1:tag2:\n" 2 1)))
+ (should (string= result "* Heading :tag1:tag2:\n"))))
+
+(ert-deftest test-demote-heading-with-todo ()
+ "Should demote heading preserving TODO keyword."
+ (let ((result (test-demote "** TODO Task\n" 2 1)))
+ (should (string= result "* TODO Task\n"))))
+
+(ert-deftest test-demote-heading-with-priority ()
+ "Should demote heading preserving priority."
+ (let ((result (test-demote "** [#A] Important\n" 2 1)))
+ (should (string= result "* [#A] Important\n"))))
+
+;;; Edge Cases - Whitespace
+
+(ert-deftest test-demote-preserves-indentation ()
+ "Should preserve indentation in body text."
+ (let ((result (test-demote "** Heading\n Indented text\n" 2 1)))
+ (should (string= result "* Heading\n Indented text\n"))))
+
+(ert-deftest test-demote-multiple-spaces-after-stars ()
+ "Should handle multiple spaces after stars."
+ (let ((result (test-demote "** Heading\n" 2 1)))
+ (should (string= result "* Heading\n"))))
+
+;;; Edge Cases - Large Demotion
+
+(ert-deftest test-demote-large-level-difference ()
+ "Should handle large level differences."
+ (let ((result (test-demote "****** Level 6\n******* Level 7\n" 6 1)))
+ (should (string= result "* Level 6\n** Level 7\n"))))
+
+(ert-deftest test-demote-to-level-2 ()
+ "Should demote to level 2 when specified."
+ (let ((result (test-demote "***** Level 5\n****** Level 6\n" 5 2)))
+ (should (string= result "** Level 5\n*** Level 6\n"))))
+
+(provide 'test-org-roam-config-demote)
+;;; test-org-roam-config-demote.el ends here
diff --git a/tests/test-org-roam-config-format.el b/tests/test-org-roam-config-format.el
new file mode 100644
index 00000000..e9378b7a
--- /dev/null
+++ b/tests/test-org-roam-config-format.el
@@ -0,0 +1,151 @@
+;;; test-org-roam-config-format.el --- Tests for cj/--format-roam-node -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; Tests for the cj/--format-roam-node function from org-roam-config.el
+;;
+;; This function formats org-roam node file content with title, node-id, and body content.
+;; It creates a complete org-roam file with properties, title, category, and filetags.
+;;
+;; Example:
+;; Input: title: "My Note", node-id: "abc123", content: "* Content\n"
+;; Output: Full org-roam file with metadata and content
+
+;;; Code:
+
+(require 'ert)
+(require 'testutil-general)
+
+;; Add modules directory to load path
+(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory))
+
+;; Now load the actual production module
+(require 'org-roam-config)
+
+;;; Test Helpers
+
+(defun test-format (title node-id content)
+ "Test cj/--format-roam-node with TITLE, NODE-ID, and CONTENT.
+Returns the formatted file content."
+ (cj/--format-roam-node title node-id content))
+
+;;; Normal Cases
+
+(ert-deftest test-format-simple-node ()
+ "Should format simple node with all components."
+ (let ((result (test-format "Test Title" "id-123" "* Content\n")))
+ (should (string-match-p ":PROPERTIES:" result))
+ (should (string-match-p ":ID: id-123" result))
+ (should (string-match-p "#\\+TITLE: Test Title" result))
+ (should (string-match-p "#\\+CATEGORY: Test Title" result))
+ (should (string-match-p "#\\+FILETAGS: Topic" result))
+ (should (string-match-p "\\* Content" result))))
+
+(ert-deftest test-format-properties-first ()
+ "Should place properties at the beginning."
+ (let ((result (test-format "Title" "id" "content")))
+ (should (string-prefix-p ":PROPERTIES:\n" result))))
+
+(ert-deftest test-format-id-after-properties ()
+ "Should place ID in properties block."
+ (let ((result (test-format "Title" "test-id-456" "content")))
+ (should (string-match-p ":PROPERTIES:\n:ID: test-id-456\n:END:" result))))
+
+(ert-deftest test-format-title-after-properties ()
+ "Should place title after properties."
+ (let ((result (test-format "My Title" "id" "content")))
+ (should (string-match-p ":END:\n#\\+TITLE: My Title\n" result))))
+
+(ert-deftest test-format-category-matches-title ()
+ "Should set category to match title."
+ (let ((result (test-format "Project Name" "id" "content")))
+ (should (string-match-p "#\\+TITLE: Project Name\n#\\+CATEGORY: Project Name\n" result))))
+
+(ert-deftest test-format-filetags-topic ()
+ "Should set filetags to Topic."
+ (let ((result (test-format "Title" "id" "content")))
+ (should (string-match-p "#\\+FILETAGS: Topic\n" result))))
+
+(ert-deftest test-format-content-at-end ()
+ "Should place content after metadata."
+ (let ((result (test-format "Title" "id" "* Heading\nBody text\n")))
+ (should (string-suffix-p "* Heading\nBody text\n" result))))
+
+;;; Edge Cases - Various Titles
+
+(ert-deftest test-format-title-with-spaces ()
+ "Should handle title with spaces."
+ (let ((result (test-format "Multi Word Title" "id" "content")))
+ (should (string-match-p "#\\+TITLE: Multi Word Title" result))
+ (should (string-match-p "#\\+CATEGORY: Multi Word Title" result))))
+
+(ert-deftest test-format-title-with-punctuation ()
+ "Should handle title with punctuation."
+ (let ((result (test-format "Title: With, Punctuation!" "id" "content")))
+ (should (string-match-p "#\\+TITLE: Title: With, Punctuation!" result))))
+
+(ert-deftest test-format-title-with-numbers ()
+ "Should handle title with numbers."
+ (let ((result (test-format "Version 2.0" "id" "content")))
+ (should (string-match-p "#\\+TITLE: Version 2\\.0" result))))
+
+;;; Edge Cases - Various Node IDs
+
+(ert-deftest test-format-uuid-style-id ()
+ "Should handle UUID-style ID."
+ (let ((result (test-format "Title" "a1b2c3d4-e5f6-7890-abcd-ef1234567890" "content")))
+ (should (string-match-p ":ID: a1b2c3d4-e5f6-7890-abcd-ef1234567890" result))))
+
+(ert-deftest test-format-short-id ()
+ "Should handle short ID."
+ (let ((result (test-format "Title" "1" "content")))
+ (should (string-match-p ":ID: 1" result))))
+
+(ert-deftest test-format-long-id ()
+ "Should handle long ID."
+ (let* ((long-id (make-string 100 ?a))
+ (result (test-format "Title" long-id "content")))
+ (should (string-match-p (concat ":ID: " long-id) result))))
+
+;;; Edge Cases - Various Content
+
+(ert-deftest test-format-empty-content ()
+ "Should handle empty content."
+ (let ((result (test-format "Title" "id" "")))
+ (should (string-suffix-p "#+FILETAGS: Topic\n\n" result))))
+
+(ert-deftest test-format-multiline-content ()
+ "Should handle multiline content."
+ (let ((result (test-format "Title" "id" "* H1\nText\n** H2\nMore\n")))
+ (should (string-suffix-p "* H1\nText\n** H2\nMore\n" result))))
+
+(ert-deftest test-format-content-with-properties ()
+ "Should handle content that already has properties."
+ (let ((result (test-format "Title" "id" "* Heading\n:PROPERTIES:\n:CUSTOM: value\n:END:\n")))
+ (should (string-match-p ":CUSTOM: value" result))))
+
+;;; Integration Tests - Structure
+
+(ert-deftest test-format-complete-structure ()
+ "Should create proper org-roam file structure."
+ (let ((result (test-format "My Note" "abc-123" "* Content\n")))
+ ;; Check order of components
+ (should (< (string-match ":PROPERTIES:" result)
+ (string-match ":ID:" result)))
+ (should (< (string-match ":ID:" result)
+ (string-match ":END:" result)))
+ (should (< (string-match ":END:" result)
+ (string-match "#\\+TITLE:" result)))
+ (should (< (string-match "#\\+TITLE:" result)
+ (string-match "#\\+CATEGORY:" result)))
+ (should (< (string-match "#\\+CATEGORY:" result)
+ (string-match "#\\+FILETAGS:" result)))
+ (should (< (string-match "#\\+FILETAGS:" result)
+ (string-match "\\* Content" result)))))
+
+(ert-deftest test-format-double-newline-after-metadata ()
+ "Should have double newline between metadata and content."
+ (let ((result (test-format "Title" "id" "* Content")))
+ (should (string-match-p "#\\+FILETAGS: Topic\n\n\\* Content" result))))
+
+(provide 'test-org-roam-config-format)
+;;; test-org-roam-config-format.el ends here
diff --git a/tests/test-org-roam-config-link-description.el b/tests/test-org-roam-config-link-description.el
new file mode 100644
index 00000000..06321b8f
--- /dev/null
+++ b/tests/test-org-roam-config-link-description.el
@@ -0,0 +1,188 @@
+;;; test-org-roam-config-link-description.el --- Tests for cj/org-link-get-description -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; Tests for the cj/org-link-get-description function from org-roam-config.el
+;;
+;; This function extracts the description from an org link, or returns the text unchanged.
+;; If TEXT contains an org link like [[url][description]], it returns description.
+;; If TEXT contains multiple links, only the first one is processed.
+;; Otherwise it returns TEXT unchanged.
+;;
+;; Examples:
+;; Input: "[[https://example.com][Example Site]]"
+;; Output: "Example Site"
+;;
+;; Input: "[[https://example.com]]"
+;; Output: "https://example.com"
+;;
+;; Input: "Plain text"
+;; Output: "Plain text"
+
+;;; Code:
+
+(require 'ert)
+(require 'testutil-general)
+
+;; Add modules directory to load path
+(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory))
+
+;; Now load the actual production module
+(require 'org-roam-config)
+
+;;; Test Helpers
+
+(defun test-link-description (text)
+ "Test cj/org-link-get-description on TEXT.
+Returns the extracted description or text unchanged."
+ (cj/org-link-get-description text))
+
+;;; Normal Cases - Link with Description
+
+(ert-deftest test-link-with-description ()
+ "Should extract description from link with description."
+ (let ((result (test-link-description "[[https://example.com][Example Site]]")))
+ (should (string= result "Example Site"))))
+
+(ert-deftest test-link-with-multiword-description ()
+ "Should extract multi-word description."
+ (let ((result (test-link-description "[[url][Multiple Word Description]]")))
+ (should (string= result "Multiple Word Description"))))
+
+(ert-deftest test-link-with-special-chars-in-description ()
+ "Should extract description with special characters."
+ (let ((result (test-link-description "[[url][Description: with, punctuation!]]")))
+ (should (string= result "Description: with, punctuation!"))))
+
+(ert-deftest test-link-file-path-with-description ()
+ "Should extract description from file link."
+ (let ((result (test-link-description "[[file:~/document.pdf][My Document]]")))
+ (should (string= result "My Document"))))
+
+(ert-deftest test-link-with-numbers-in-description ()
+ "Should extract description containing numbers."
+ (let ((result (test-link-description "[[url][Chapter 42]]")))
+ (should (string= result "Chapter 42"))))
+
+;;; Normal Cases - Link without Description
+
+(ert-deftest test-link-without-description-url ()
+ "Should return URL when no description is present."
+ (let ((result (test-link-description "[[https://example.com]]")))
+ (should (string= result "https://example.com"))))
+
+(ert-deftest test-link-without-description-file ()
+ "Should return file path when no description."
+ (let ((result (test-link-description "[[file:~/notes.org]]")))
+ (should (string= result "file:~/notes.org"))))
+
+(ert-deftest test-link-without-description-id ()
+ "Should return ID when no description."
+ (let ((result (test-link-description "[[id:abc123]]")))
+ (should (string= result "id:abc123"))))
+
+;;; Normal Cases - No Link
+
+(ert-deftest test-plain-text ()
+ "Should return plain text unchanged."
+ (let ((result (test-link-description "Plain text without link")))
+ (should (string= result "Plain text without link"))))
+
+(ert-deftest test-text-with-brackets-but-not-link ()
+ "Should return text with single brackets unchanged."
+ (let ((result (test-link-description "Text [with] brackets")))
+ (should (string= result "Text [with] brackets"))))
+
+(ert-deftest test-text-with-partial-link-syntax ()
+ "Should return text with partial link syntax unchanged."
+ (let ((result (test-link-description "[[incomplete link")))
+ (should (string= result "[[incomplete link"))))
+
+;;; Boundary Cases - Multiple Links
+
+(ert-deftest test-multiple-links-extracts-first ()
+ "Should extract description from first link only."
+ (let ((result (test-link-description "[[url1][First]] and [[url2][Second]]")))
+ (should (string= result "First"))))
+
+(ert-deftest test-multiple-links-first-has-no-description ()
+ "Should extract URL from first link when it has no description."
+ (let ((result (test-link-description "[[url1]] and [[url2][Second]]")))
+ (should (string= result "url1"))))
+
+;;; Boundary Cases - Empty and Edge Cases
+
+(ert-deftest test-empty-string ()
+ "Should return empty string unchanged."
+ (let ((result (test-link-description "")))
+ (should (string= result ""))))
+
+(ert-deftest test-link-with-empty-description ()
+ "Should return text unchanged when description brackets are empty."
+ (let ((result (test-link-description "[[https://example.com][]]")))
+ ;; Regex requires at least one char in description, so no match
+ (should (string= result "[[https://example.com][]]"))))
+
+(ert-deftest test-link-with-empty-url ()
+ "Should return text unchanged when link is completely empty."
+ (let ((result (test-link-description "[[]]")))
+ ;; Regex requires at least one char in URL, so no match, returns unchanged
+ (should (string= result "[[]]"))))
+
+(ert-deftest test-link-with-empty-url-and-description ()
+ "Should handle completely empty link."
+ (let ((result (test-link-description "[][]")))
+ (should (string= result "[][]"))))
+
+;;; Edge Cases - Special Link Types
+
+(ert-deftest test-internal-link ()
+ "Should extract description from internal link."
+ (let ((result (test-link-description "[[*Heading][My Heading]]")))
+ (should (string= result "My Heading"))))
+
+(ert-deftest test-internal-link-without-description ()
+ "Should return heading target from internal link without description."
+ (let ((result (test-link-description "[[*Heading]]")))
+ (should (string= result "*Heading"))))
+
+(ert-deftest test-custom-id-link ()
+ "Should handle custom ID links."
+ (let ((result (test-link-description "[[#custom-id][Custom Section]]")))
+ (should (string= result "Custom Section"))))
+
+;;; Edge Cases - Link with Surrounding Text
+
+(ert-deftest test-link-with-prefix-text ()
+ "Should extract description from link with prefix text."
+ (let ((result (test-link-description "See [[url][documentation]] for details")))
+ (should (string= result "documentation"))))
+
+(ert-deftest test-link-at-start ()
+ "Should extract description from link at start of text."
+ (let ((result (test-link-description "[[url][Link]] at beginning")))
+ (should (string= result "Link"))))
+
+(ert-deftest test-link-at-end ()
+ "Should extract description from link at end of text."
+ (let ((result (test-link-description "Text with [[url][link]]")))
+ (should (string= result "link"))))
+
+;;; Edge Cases - Special Characters in URL
+
+(ert-deftest test-link-with-query-params ()
+ "Should handle URL with query parameters."
+ (let ((result (test-link-description "[[https://example.com?q=test&foo=bar][Search]]")))
+ (should (string= result "Search"))))
+
+(ert-deftest test-link-with-anchor ()
+ "Should handle URL with anchor."
+ (let ((result (test-link-description "[[https://example.com#section][Section]]")))
+ (should (string= result "Section"))))
+
+(ert-deftest test-link-with-spaces-in-description ()
+ "Should preserve spaces in description."
+ (let ((result (test-link-description "[[url][Multiple Spaces]]")))
+ (should (string= result "Multiple Spaces"))))
+
+(provide 'test-org-roam-config-link-description)
+;;; test-org-roam-config-link-description.el ends here
diff --git a/tests/test-org-roam-config-slug.el b/tests/test-org-roam-config-slug.el
new file mode 100644
index 00000000..eb3149dd
--- /dev/null
+++ b/tests/test-org-roam-config-slug.el
@@ -0,0 +1,223 @@
+;;; test-org-roam-config-slug.el --- Tests for cj/--generate-roam-slug -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; Tests for the cj/--generate-roam-slug function from org-roam-config.el
+;;
+;; This function converts a title to a filename-safe slug by:
+;; 1. Converting to lowercase
+;; 2. Replacing non-alphanumeric characters with hyphens
+;; 3. Removing leading and trailing hyphens
+;;
+;; Examples:
+;; Input: "My Project Name"
+;; Output: "my-project-name"
+;;
+;; Input: "Hello, World!"
+;; Output: "hello-world"
+
+;;; Code:
+
+(require 'ert)
+(require 'testutil-general)
+
+;; Add modules directory to load path
+(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory))
+
+;; Now load the actual production module
+(require 'org-roam-config)
+
+;;; Test Helpers
+
+(defun test-slug (title)
+ "Test cj/--generate-roam-slug on TITLE.
+Returns the slugified string."
+ (cj/--generate-roam-slug title))
+
+;;; Normal Cases - Simple Titles
+
+(ert-deftest test-slug-simple-word ()
+ "Should return lowercase simple word."
+ (let ((result (test-slug "Hello")))
+ (should (string= result "hello"))))
+
+(ert-deftest test-slug-multiple-words ()
+ "Should replace spaces with hyphens."
+ (let ((result (test-slug "My Project Name")))
+ (should (string= result "my-project-name"))))
+
+(ert-deftest test-slug-already-lowercase ()
+ "Should handle already lowercase text."
+ (let ((result (test-slug "simple")))
+ (should (string= result "simple"))))
+
+(ert-deftest test-slug-mixed-case ()
+ "Should convert mixed case to lowercase."
+ (let ((result (test-slug "MixedCaseTitle")))
+ (should (string= result "mixedcasetitle"))))
+
+;;; Normal Cases - Punctuation
+
+(ert-deftest test-slug-with-comma ()
+ "Should remove commas."
+ (let ((result (test-slug "Hello, World")))
+ (should (string= result "hello-world"))))
+
+(ert-deftest test-slug-with-period ()
+ "Should remove periods."
+ (let ((result (test-slug "Version 2.0")))
+ (should (string= result "version-2-0"))))
+
+(ert-deftest test-slug-with-exclamation ()
+ "Should remove exclamation marks."
+ (let ((result (test-slug "Hello World!")))
+ (should (string= result "hello-world"))))
+
+(ert-deftest test-slug-with-question ()
+ "Should remove question marks."
+ (let ((result (test-slug "What Is This?")))
+ (should (string= result "what-is-this"))))
+
+(ert-deftest test-slug-with-colon ()
+ "Should remove colons."
+ (let ((result (test-slug "Note: Important")))
+ (should (string= result "note-important"))))
+
+(ert-deftest test-slug-with-parentheses ()
+ "Should remove parentheses."
+ (let ((result (test-slug "Item (copy)")))
+ (should (string= result "item-copy"))))
+
+;;; Normal Cases - Numbers
+
+(ert-deftest test-slug-with-numbers ()
+ "Should preserve numbers."
+ (let ((result (test-slug "Chapter 42")))
+ (should (string= result "chapter-42"))))
+
+(ert-deftest test-slug-only-numbers ()
+ "Should handle titles with only numbers."
+ (let ((result (test-slug "123")))
+ (should (string= result "123"))))
+
+(ert-deftest test-slug-mixed-alphanumeric ()
+ "Should preserve alphanumeric characters."
+ (let ((result (test-slug "Test123ABC")))
+ (should (string= result "test123abc"))))
+
+;;; Boundary Cases - Multiple Consecutive Special Chars
+
+(ert-deftest test-slug-multiple-spaces ()
+ "Should collapse multiple spaces into single hyphen."
+ (let ((result (test-slug "Hello World")))
+ (should (string= result "hello-world"))))
+
+(ert-deftest test-slug-mixed-punctuation ()
+ "Should collapse mixed punctuation into single hyphen."
+ (let ((result (test-slug "Hello, ... World!")))
+ (should (string= result "hello-world"))))
+
+(ert-deftest test-slug-consecutive-hyphens ()
+ "Should collapse consecutive hyphens."
+ (let ((result (test-slug "Hello---World")))
+ (should (string= result "hello-world"))))
+
+;;; Boundary Cases - Leading/Trailing Special Chars
+
+(ert-deftest test-slug-leading-space ()
+ "Should remove leading hyphen from leading space."
+ (let ((result (test-slug " Hello")))
+ (should (string= result "hello"))))
+
+(ert-deftest test-slug-trailing-space ()
+ "Should remove trailing hyphen from trailing space."
+ (let ((result (test-slug "Hello ")))
+ (should (string= result "hello"))))
+
+(ert-deftest test-slug-leading-punctuation ()
+ "Should remove leading hyphen from leading punctuation."
+ (let ((result (test-slug "...Hello")))
+ (should (string= result "hello"))))
+
+(ert-deftest test-slug-trailing-punctuation ()
+ "Should remove trailing hyphen from trailing punctuation."
+ (let ((result (test-slug "Hello!!!")))
+ (should (string= result "hello"))))
+
+(ert-deftest test-slug-leading-and-trailing ()
+ "Should remove both leading and trailing hyphens."
+ (let ((result (test-slug " Hello World ")))
+ (should (string= result "hello-world"))))
+
+;;; Boundary Cases - Empty and Short
+
+(ert-deftest test-slug-empty-string ()
+ "Should return empty string for empty input."
+ (let ((result (test-slug "")))
+ (should (string= result ""))))
+
+(ert-deftest test-slug-only-punctuation ()
+ "Should return empty string for only punctuation."
+ (let ((result (test-slug "!!!")))
+ (should (string= result ""))))
+
+(ert-deftest test-slug-only-spaces ()
+ "Should return empty string for only spaces."
+ (let ((result (test-slug " ")))
+ (should (string= result ""))))
+
+(ert-deftest test-slug-single-char ()
+ "Should handle single character."
+ (let ((result (test-slug "A")))
+ (should (string= result "a"))))
+
+;;; Edge Cases - Special Characters
+
+(ert-deftest test-slug-with-underscore ()
+ "Should replace underscores with hyphens."
+ (let ((result (test-slug "my_variable_name")))
+ (should (string= result "my-variable-name"))))
+
+(ert-deftest test-slug-with-slash ()
+ "Should remove slashes."
+ (let ((result (test-slug "path/to/file")))
+ (should (string= result "path-to-file"))))
+
+(ert-deftest test-slug-with-at-sign ()
+ "Should remove at signs."
+ (let ((result (test-slug "user@example")))
+ (should (string= result "user-example"))))
+
+(ert-deftest test-slug-with-hash ()
+ "Should remove hash symbols."
+ (let ((result (test-slug "#hashtag")))
+ (should (string= result "hashtag"))))
+
+(ert-deftest test-slug-with-dollar ()
+ "Should remove dollar signs."
+ (let ((result (test-slug "$price")))
+ (should (string= result "price"))))
+
+;;; Edge Cases - Unicode (if supported)
+
+(ert-deftest test-slug-with-unicode ()
+ "Should remove unicode characters."
+ (let ((result (test-slug "Café")))
+ (should (string= result "caf"))))
+
+(ert-deftest test-slug-with-emoji ()
+ "Should remove emoji."
+ (let ((result (test-slug "Hello 😀 World")))
+ (should (string= result "hello-world"))))
+
+;;; Edge Cases - Long Titles
+
+(ert-deftest test-slug-very-long-title ()
+ "Should handle very long titles."
+ (let* ((long-title (mapconcat #'identity (make-list 20 "word") " "))
+ (result (test-slug long-title)))
+ (should (string-prefix-p "word-" result))
+ (should (string-suffix-p "-word" result))
+ (should (not (string-match-p " " result)))))
+
+(provide 'test-org-roam-config-slug)
+;;; test-org-roam-config-slug.el ends here
diff --git a/tests/test-org-webclipper-process.el b/tests/test-org-webclipper-process.el
new file mode 100644
index 00000000..9a25ef5c
--- /dev/null
+++ b/tests/test-org-webclipper-process.el
@@ -0,0 +1,210 @@
+;;; test-org-webclipper-process.el --- Tests for cj/--process-webclip-content -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; Tests for the cj/--process-webclip-content function from org-webclipper.el
+;;
+;; This function processes webclipped org-mode content by:
+;; 1. Removing the first top-level heading
+;; 2. Removing any initial blank lines
+;; 3. Demoting all remaining headings by one level
+;;
+;; Examples:
+;; Input: "* Title\nContent\n** Sub\n"
+;; Output: "Content\n*** Sub\n"
+;;
+;; Input: "* Title\n\n\n** Sub\n"
+;; Output: "*** Sub\n"
+
+;;; Code:
+
+(require 'ert)
+(require 'testutil-general)
+
+;; Add modules directory to load path
+(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory))
+
+;; Now load the actual production module
+(require 'org-webclipper)
+
+;;; Test Helpers
+
+(defun test-process-webclip (content)
+ "Test cj/--process-webclip-content on CONTENT.
+Returns the processed content."
+ (cj/--process-webclip-content content))
+
+;;; Normal Cases - Single Heading Removal
+
+(ert-deftest test-process-removes-first-heading ()
+ "Should remove the first top-level heading."
+ (let ((result (test-process-webclip "* Title\nContent\n")))
+ (should (string= result "Content\n"))))
+
+(ert-deftest test-process-removes-heading-with-text ()
+ "Should remove first heading preserving body text."
+ (let ((result (test-process-webclip "* Page Title\nParagraph text\n")))
+ (should (string= result "Paragraph text\n"))))
+
+(ert-deftest test-process-removes-heading-with-tags ()
+ "Should remove first heading even with tags."
+ (let ((result (test-process-webclip "* Title :tag1:tag2:\nContent\n")))
+ (should (string= result "Content\n"))))
+
+(ert-deftest test-process-removes-heading-with-todo ()
+ "Should remove first heading even with TODO keyword."
+ (let ((result (test-process-webclip "* TODO Task\nContent\n")))
+ (should (string= result "Content\n"))))
+
+;;; Normal Cases - Blank Line Removal
+
+(ert-deftest test-process-removes-single-blank-line ()
+ "Should remove single blank line after heading removal."
+ (let ((result (test-process-webclip "* Title\n\nContent\n")))
+ (should (string= result "Content\n"))))
+
+(ert-deftest test-process-removes-multiple-blank-lines ()
+ "Should remove multiple blank lines after heading removal."
+ (let ((result (test-process-webclip "* Title\n\n\n\nContent\n")))
+ (should (string= result "Content\n"))))
+
+(ert-deftest test-process-removes-blank-lines-with-spaces ()
+ "Should remove blank lines that contain only spaces."
+ (let ((result (test-process-webclip "* Title\n \n\t\nContent\n")))
+ (should (string= result "Content\n"))))
+
+(ert-deftest test-process-preserves-blank-lines-in-content ()
+ "Should preserve blank lines within the content."
+ (let ((result (test-process-webclip "* Title\nPara 1\n\nPara 2\n")))
+ (should (string= result "Para 1\n\nPara 2\n"))))
+
+;;; Normal Cases - Heading Demotion
+
+(ert-deftest test-process-demotes-second-level ()
+ "Should demote level 2 heading to level 3."
+ (let ((result (test-process-webclip "* Title\n** Section\n")))
+ (should (string= result "*** Section\n"))))
+
+(ert-deftest test-process-demotes-third-level ()
+ "Should demote level 3 heading to level 4."
+ (let ((result (test-process-webclip "* Title\n*** Subsection\n")))
+ (should (string= result "**** Subsection\n"))))
+
+(ert-deftest test-process-demotes-multiple-headings ()
+ "Should demote all headings in the content."
+ (let ((result (test-process-webclip "* Title\n** Section 1\n** Section 2\n")))
+ (should (string= result "*** Section 1\n*** Section 2\n"))))
+
+(ert-deftest test-process-demotes-nested-hierarchy ()
+ "Should demote nested heading structure."
+ (let ((result (test-process-webclip "* Title\n** Section\n*** Subsection\n")))
+ (should (string= result "*** Section\n**** Subsection\n"))))
+
+;;; Normal Cases - Combined Processing
+
+(ert-deftest test-process-full-workflow ()
+ "Should remove heading, blank lines, and demote remaining headings."
+ (let ((result (test-process-webclip "* Article Title\n\n** Introduction\nText\n** Conclusion\n")))
+ (should (string= result "*** Introduction\nText\n*** Conclusion\n"))))
+
+(ert-deftest test-process-with-properties ()
+ "Should preserve properties in demoted headings."
+ (let ((result (test-process-webclip "* Title\n** Heading\n:PROPERTIES:\n:ID: 123\n:END:\n")))
+ (should (string= result "*** Heading\n:PROPERTIES:\n:ID: 123\n:END:\n"))))
+
+(ert-deftest test-process-with-mixed-content ()
+ "Should handle mixed text and headings."
+ (let ((result (test-process-webclip "* Title\nIntro text\n** Section\nBody text\n")))
+ (should (string= result "Intro text\n*** Section\nBody text\n"))))
+
+;;; Edge Cases - Empty and Minimal Content
+
+(ert-deftest test-process-empty-string ()
+ "Should return empty string for empty input."
+ (let ((result (test-process-webclip "")))
+ (should (string= result ""))))
+
+(ert-deftest test-process-only-heading ()
+ "Should return empty string when only first heading present."
+ (let ((result (test-process-webclip "* Title\n")))
+ (should (string= result ""))))
+
+(ert-deftest test-process-only-blank-lines ()
+ "Should return empty string for only blank lines after heading."
+ (let ((result (test-process-webclip "* Title\n\n\n")))
+ (should (string= result ""))))
+
+(ert-deftest test-process-no-heading ()
+ "Should handle content without any heading."
+ (let ((result (test-process-webclip "Just plain text\n")))
+ (should (string= result "Just plain text\n"))))
+
+(ert-deftest test-process-heading-no-newline ()
+ "Should demote heading without trailing newline (doesn't match removal pattern)."
+ (let ((result (test-process-webclip "* Title")))
+ (should (string= result "** Title"))))
+
+;;; Edge Cases - Heading Variations
+
+(ert-deftest test-process-heading-without-space ()
+ "Should not match heading without space after stars."
+ (let ((result (test-process-webclip "*Title\nContent\n")))
+ (should (string= result "*Title\nContent\n"))))
+
+(ert-deftest test-process-multiple-top-level-headings ()
+ "Should only remove first top-level heading."
+ (let ((result (test-process-webclip "* Title 1\n* Title 2\n")))
+ (should (string= result "** Title 2\n"))))
+
+(ert-deftest test-process-heading-with-priority ()
+ "Should remove heading with priority marker."
+ (let ((result (test-process-webclip "* [#A] Important\nContent\n")))
+ (should (string= result "Content\n"))))
+
+(ert-deftest test-process-heading-with-links ()
+ "Should remove heading containing links."
+ (let ((result (test-process-webclip "* [[url][Link Title]]\nContent\n")))
+ (should (string= result "Content\n"))))
+
+;;; Edge Cases - Special Content
+
+(ert-deftest test-process-preserves-lists ()
+ "Should preserve list formatting."
+ (let ((result (test-process-webclip "* Title\n- Item 1\n- Item 2\n")))
+ (should (string= result "- Item 1\n- Item 2\n"))))
+
+(ert-deftest test-process-preserves-code-blocks ()
+ "Should preserve code block content."
+ (let ((result (test-process-webclip "* Title\n#+BEGIN_SRC python\nprint('hi')\n#+END_SRC\n")))
+ (should (string= result "#+BEGIN_SRC python\nprint('hi')\n#+END_SRC\n"))))
+
+(ert-deftest test-process-preserves-tables ()
+ "Should preserve org table content."
+ (let ((result (test-process-webclip "* Title\n| A | B |\n| 1 | 2 |\n")))
+ (should (string= result "| A | B |\n| 1 | 2 |\n"))))
+
+;;; Edge Cases - Deep Nesting
+
+(ert-deftest test-process-very-deep-headings ()
+ "Should demote very deep heading structures."
+ (let ((result (test-process-webclip "* Title\n****** Level 6\n")))
+ (should (string= result "******* Level 6\n"))))
+
+(ert-deftest test-process-complex-document ()
+ "Should handle complex document structure."
+ (let ((result (test-process-webclip "* Main Title\n\n** Section 1\nText 1\n*** Subsection 1.1\nText 2\n** Section 2\nText 3\n")))
+ (should (string= result "*** Section 1\nText 1\n**** Subsection 1.1\nText 2\n*** Section 2\nText 3\n"))))
+
+;;; Integration Tests
+
+(ert-deftest test-process-realistic-webpage ()
+ "Should process realistic webclipped content."
+ (let ((result (test-process-webclip "* How to Program in Emacs Lisp\n\n** Introduction\nEmacs Lisp is powerful.\n\n** Getting Started\nFirst, open Emacs.\n\n*** Installation\nDownload from gnu.org\n")))
+ (should (string= result "*** Introduction\nEmacs Lisp is powerful.\n\n*** Getting Started\nFirst, open Emacs.\n\n**** Installation\nDownload from gnu.org\n"))))
+
+(ert-deftest test-process-article-with-metadata ()
+ "Should handle article with org metadata."
+ (let ((result (test-process-webclip "* Article Title :article:web:\n#+DATE: 2024-01-01\n\n** Content\nBody text\n")))
+ (should (string= result "#+DATE: 2024-01-01\n\n*** Content\nBody text\n"))))
+
+(provide 'test-org-webclipper-process)
+;;; test-org-webclipper-process.el ends here
diff --git a/tests/test-test-runner.el b/tests/test-test-runner.el
new file mode 100644
index 00000000..0edc0d65
--- /dev/null
+++ b/tests/test-test-runner.el
@@ -0,0 +1,359 @@
+;;; test-test-runner.el --- Tests for test-runner.el -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; Unit tests for test-runner.el - ERT test runner with focus/unfocus workflow.
+;;
+;; Testing approach:
+;; - Tests focus on internal `cj/test--do-*` functions (pure business logic)
+;; - File system operations use temp directories
+;; - Tests are isolated with setup/teardown
+;; - Tests verify return values, not user messages
+
+;;; Code:
+
+(require 'ert)
+(require 'testutil-general)
+
+;; Add modules directory to load path
+(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory))
+
+;; Load the module (ignore keymap error in batch mode)
+(condition-case nil
+ (require 'test-runner)
+ (error nil))
+
+;;; Test Utilities
+
+(defvar test-testrunner--temp-dir nil
+ "Temporary directory for test files during tests.")
+
+(defvar test-testrunner--original-focused-files nil
+ "Backup of focused files list before test.")
+
+(defun test-testrunner-setup ()
+ "Setup test environment before each test."
+ ;; Backup current state
+ (setq test-testrunner--original-focused-files cj/test-focused-files)
+ ;; Reset to clean state
+ (setq cj/test-focused-files '())
+ ;; Create temp directory for file tests
+ (setq test-testrunner--temp-dir (make-temp-file "test-runner-test" t)))
+
+(defun test-testrunner-teardown ()
+ "Clean up test environment after each test."
+ ;; Restore state
+ (setq cj/test-focused-files test-testrunner--original-focused-files)
+ ;; Clean up temp directory
+ (when (and test-testrunner--temp-dir
+ (file-directory-p test-testrunner--temp-dir))
+ (delete-directory test-testrunner--temp-dir t))
+ (setq test-testrunner--temp-dir nil))
+
+(defun test-testrunner-create-test-file (filename content)
+ "Create test file FILENAME with CONTENT in temp directory."
+ (let ((filepath (expand-file-name filename test-testrunner--temp-dir)))
+ (with-temp-file filepath
+ (insert content))
+ filepath))
+
+;;; Normal Cases - Load Files
+
+(ert-deftest test-testrunner-load-files-success ()
+ "Should successfully load test files."
+ (test-testrunner-setup)
+ (let* ((file1 (test-testrunner-create-test-file "test-simple.el"
+ "(defun test-func () t)"))
+ (file2 (test-testrunner-create-test-file "test-other.el"
+ "(defun other-func () nil)"))
+ (result (cj/test--do-load-files test-testrunner--temp-dir
+ (list file1 file2))))
+ (should (eq (car result) 'success))
+ (should (= (cdr result) 2)))
+ (test-testrunner-teardown))
+
+(ert-deftest test-testrunner-load-files-with-errors ()
+ "Should handle errors during file loading."
+ (test-testrunner-setup)
+ (let* ((good-file (test-testrunner-create-test-file "test-good.el"
+ "(defun good () t)"))
+ (bad-file (test-testrunner-create-test-file "test-bad.el"
+ "(defun bad ( "))
+ (result (cj/test--do-load-files test-testrunner--temp-dir
+ (list good-file bad-file))))
+ (should (eq (car result) 'error))
+ (should (= (nth 1 result) 1)) ; loaded-count
+ (should (= (length (nth 2 result)) 1))) ; errors list
+ (test-testrunner-teardown))
+
+;;; Normal Cases - Focus Add
+
+(ert-deftest test-testrunner-focus-add-success ()
+ "Should successfully add file to focus."
+ (test-testrunner-setup)
+ (let ((result (cj/test--do-focus-add "test-foo.el"
+ '("test-foo.el" "test-bar.el")
+ '())))
+ (should (eq result 'success)))
+ (test-testrunner-teardown))
+
+(ert-deftest test-testrunner-focus-add-already-focused ()
+ "Should detect already focused file."
+ (test-testrunner-setup)
+ (let ((result (cj/test--do-focus-add "test-foo.el"
+ '("test-foo.el" "test-bar.el")
+ '("test-foo.el"))))
+ (should (eq result 'already-focused)))
+ (test-testrunner-teardown))
+
+(ert-deftest test-testrunner-focus-add-not-available ()
+ "Should detect file not in available list."
+ (test-testrunner-setup)
+ (let ((result (cj/test--do-focus-add "test-missing.el"
+ '("test-foo.el" "test-bar.el")
+ '())))
+ (should (eq result 'not-available)))
+ (test-testrunner-teardown))
+
+;;; Normal Cases - Focus Add File
+
+(ert-deftest test-testrunner-focus-add-file-success ()
+ "Should successfully validate and add file to focus."
+ (test-testrunner-setup)
+ (let* ((filepath (expand-file-name "test-foo.el" test-testrunner--temp-dir))
+ (result (cj/test--do-focus-add-file filepath test-testrunner--temp-dir '())))
+ (should (eq (car result) 'success))
+ (should (string= (cdr result) "test-foo.el")))
+ (test-testrunner-teardown))
+
+(ert-deftest test-testrunner-focus-add-file-no-file ()
+ "Should detect nil filepath."
+ (test-testrunner-setup)
+ (let ((result (cj/test--do-focus-add-file nil test-testrunner--temp-dir '())))
+ (should (eq (car result) 'no-file)))
+ (test-testrunner-teardown))
+
+(ert-deftest test-testrunner-focus-add-file-not-in-testdir ()
+ "Should detect file outside test directory."
+ (test-testrunner-setup)
+ (let* ((filepath "/tmp/outside-test.el")
+ (result (cj/test--do-focus-add-file filepath test-testrunner--temp-dir '())))
+ (should (eq (car result) 'not-in-testdir)))
+ (test-testrunner-teardown))
+
+(ert-deftest test-testrunner-focus-add-file-already-focused ()
+ "Should detect already focused file."
+ (test-testrunner-setup)
+ (let* ((filepath (expand-file-name "test-foo.el" test-testrunner--temp-dir))
+ (result (cj/test--do-focus-add-file filepath
+ test-testrunner--temp-dir
+ '("test-foo.el"))))
+ (should (eq (car result) 'already-focused))
+ (should (string= (cdr result) "test-foo.el")))
+ (test-testrunner-teardown))
+
+;;; Normal Cases - Focus Remove
+
+(ert-deftest test-testrunner-focus-remove-success ()
+ "Should successfully remove file from focus."
+ (test-testrunner-setup)
+ (let ((result (cj/test--do-focus-remove "test-foo.el" '("test-foo.el" "test-bar.el"))))
+ (should (eq result 'success)))
+ (test-testrunner-teardown))
+
+(ert-deftest test-testrunner-focus-remove-empty-list ()
+ "Should detect empty focused list."
+ (test-testrunner-setup)
+ (let ((result (cj/test--do-focus-remove "test-foo.el" '())))
+ (should (eq result 'empty-list)))
+ (test-testrunner-teardown))
+
+(ert-deftest test-testrunner-focus-remove-not-found ()
+ "Should detect file not in focused list."
+ (test-testrunner-setup)
+ (let ((result (cj/test--do-focus-remove "test-missing.el" '("test-foo.el"))))
+ (should (eq result 'not-found)))
+ (test-testrunner-teardown))
+
+;;; Normal Cases - Get Focused Tests
+
+(ert-deftest test-testrunner-get-focused-tests-success ()
+ "Should extract test names from focused files."
+ (test-testrunner-setup)
+ (let* ((file1 (test-testrunner-create-test-file "test-first.el"
+ "(ert-deftest test-alpha-one () (should t))\n(ert-deftest test-alpha-two () (should t))"))
+ (result (cj/test--do-get-focused-tests '("test-first.el") test-testrunner--temp-dir)))
+ (should (eq (car result) 'success))
+ (should (= (length (nth 1 result)) 2)) ; 2 test names
+ (should (= (nth 2 result) 1))) ; 1 file loaded
+ (test-testrunner-teardown))
+
+(ert-deftest test-testrunner-get-focused-tests-empty-list ()
+ "Should detect empty focused files list."
+ (test-testrunner-setup)
+ (let ((result (cj/test--do-get-focused-tests '() test-testrunner--temp-dir)))
+ (should (eq (car result) 'empty-list)))
+ (test-testrunner-teardown))
+
+(ert-deftest test-testrunner-get-focused-tests-no-tests ()
+ "Should detect when no tests found in files."
+ (test-testrunner-setup)
+ (test-testrunner-create-test-file "test-empty.el" "(defun not-a-test () t)")
+ (let ((result (cj/test--do-get-focused-tests '("test-empty.el") test-testrunner--temp-dir)))
+ (should (eq (car result) 'no-tests)))
+ (test-testrunner-teardown))
+
+;;; Normal Cases - Extract Test Names
+
+(ert-deftest test-testrunner-extract-test-names-simple ()
+ "Should extract test names from file."
+ (test-testrunner-setup)
+ (let* ((file (test-testrunner-create-test-file "test-simple.el"
+ "(ert-deftest test-foo () (should t))\n(ert-deftest test-bar () (should nil))"))
+ (names (cj/test--extract-test-names file)))
+ (should (= (length names) 2))
+ (should (member "test-foo" names))
+ (should (member "test-bar" names)))
+ (test-testrunner-teardown))
+
+(ert-deftest test-testrunner-extract-test-names-with-whitespace ()
+ "Should extract test names with various whitespace."
+ (test-testrunner-setup)
+ (let* ((file (test-testrunner-create-test-file "test-whitespace.el"
+ "(ert-deftest test-spaces () (should t))\n (ert-deftest test-indent () t)"))
+ (names (cj/test--extract-test-names file)))
+ (should (= (length names) 2))
+ (should (member "test-spaces" names))
+ (should (member "test-indent" names)))
+ (test-testrunner-teardown))
+
+(ert-deftest test-testrunner-extract-test-names-no-tests ()
+ "Should return empty list when no tests in file."
+ (test-testrunner-setup)
+ (let* ((file (test-testrunner-create-test-file "test-none.el"
+ "(defun not-a-test () t)"))
+ (names (cj/test--extract-test-names file)))
+ (should (null names)))
+ (test-testrunner-teardown))
+
+;;; Normal Cases - Extract Test at Position
+
+(ert-deftest test-testrunner-extract-test-at-pos-found ()
+ "Should extract test name at point."
+ (test-testrunner-setup)
+ (with-temp-buffer
+ (insert "(ert-deftest test-sample ()\n (should t))")
+ (goto-char (point-min))
+ (let ((name (cj/test--extract-test-at-pos)))
+ (should (eq name 'test-sample))))
+ (test-testrunner-teardown))
+
+(ert-deftest test-testrunner-extract-test-at-pos-not-found ()
+ "Should return nil when not in a test."
+ (test-testrunner-setup)
+ (with-temp-buffer
+ (insert "(defun regular-function ()\n (message \"hi\"))")
+ (goto-char (point-min))
+ (let ((name (cj/test--extract-test-at-pos)))
+ (should (null name))))
+ (test-testrunner-teardown))
+
+(ert-deftest test-testrunner-extract-test-at-pos-invalid-syntax ()
+ "Should return nil for invalid syntax."
+ (test-testrunner-setup)
+ (with-temp-buffer
+ (insert "(ert-deftest")
+ (goto-char (point-min))
+ (let ((name (cj/test--extract-test-at-pos)))
+ (should (null name))))
+ (test-testrunner-teardown))
+
+;;; Boundary Cases - Load Files
+
+(ert-deftest test-testrunner-load-files-empty-list ()
+ "Should handle empty file list."
+ (test-testrunner-setup)
+ (let ((result (cj/test--do-load-files test-testrunner--temp-dir '())))
+ (should (eq (car result) 'success))
+ (should (= (cdr result) 0)))
+ (test-testrunner-teardown))
+
+(ert-deftest test-testrunner-load-files-nonexistent ()
+ "Should handle nonexistent files."
+ (test-testrunner-setup)
+ (let* ((fake-file (expand-file-name "nonexistent.el" test-testrunner--temp-dir))
+ (result (cj/test--do-load-files test-testrunner--temp-dir (list fake-file))))
+ (should (eq (car result) 'error))
+ (should (= (nth 1 result) 0))) ; 0 files loaded
+ (test-testrunner-teardown))
+
+;;; Boundary Cases - Focus Add
+
+(ert-deftest test-testrunner-focus-add-single-available ()
+ "Should add when only one file available."
+ (test-testrunner-setup)
+ (let ((result (cj/test--do-focus-add "test-only.el" '("test-only.el") '())))
+ (should (eq result 'success)))
+ (test-testrunner-teardown))
+
+(ert-deftest test-testrunner-focus-add-case-sensitive ()
+ "Should be case-sensitive for filenames."
+ (test-testrunner-setup)
+ (let ((result (cj/test--do-focus-add "Test-Foo.el"
+ '("test-foo.el")
+ '())))
+ (should (eq result 'not-available)))
+ (test-testrunner-teardown))
+
+;;; Boundary Cases - Get Focused Tests
+
+(ert-deftest test-testrunner-get-focused-tests-multiple-files ()
+ "Should collect tests from multiple files."
+ (test-testrunner-setup)
+ (test-testrunner-create-test-file "test-first.el"
+ "(ert-deftest test-beta-one () t)")
+ (test-testrunner-create-test-file "test-second.el"
+ "(ert-deftest test-beta-two () t)")
+ (let ((result (cj/test--do-get-focused-tests '("test-first.el" "test-second.el")
+ test-testrunner--temp-dir)))
+ (should (eq (car result) 'success))
+ (should (= (length (nth 1 result)) 2)) ; 2 tests total
+ (should (= (nth 2 result) 2))) ; 2 files loaded
+ (test-testrunner-teardown))
+
+(ert-deftest test-testrunner-get-focused-tests-skip-nonexistent ()
+ "Should skip nonexistent files."
+ (test-testrunner-setup)
+ (test-testrunner-create-test-file "test-exists.el"
+ "(ert-deftest test-gamma-one () t)")
+ (let ((result (cj/test--do-get-focused-tests '("test-exists.el" "test-missing.el")
+ test-testrunner--temp-dir)))
+ (should (eq (car result) 'success))
+ (should (= (length (nth 1 result)) 1)) ; 1 test found
+ (should (= (nth 2 result) 1))) ; 1 file loaded (missing skipped)
+ (test-testrunner-teardown))
+
+;;; Boundary Cases - Extract Test Names
+
+(ert-deftest test-testrunner-extract-test-names-hyphens-underscores ()
+ "Should handle test names with hyphens and underscores."
+ (test-testrunner-setup)
+ (let* ((file (test-testrunner-create-test-file "test-names.el"
+ "(ert-deftest test-with-hyphens () t)\n(ert-deftest test_with_underscores () t)"))
+ (names (cj/test--extract-test-names file)))
+ (should (= (length names) 2))
+ (should (member "test-with-hyphens" names))
+ (should (member "test_with_underscores" names)))
+ (test-testrunner-teardown))
+
+(ert-deftest test-testrunner-extract-test-names-ignore-comments ()
+ "Should not extract test names from comments."
+ (test-testrunner-setup)
+ (let* ((file (test-testrunner-create-test-file "test-comments.el"
+ ";; (ert-deftest test-commented () t)\n(ert-deftest test-real () t)"))
+ (names (cj/test--extract-test-names file)))
+ (should (= (length names) 1))
+ (should (member "test-real" names)))
+ (test-testrunner-teardown))
+
+(provide 'test-test-runner)
+;;; test-test-runner.el ends here
diff --git a/tests/test-theme-theme-persistence.el.disabled b/tests/test-theme-theme-persistence.el.disabled
deleted file mode 100644
index e0b2f9e3..00000000
--- a/tests/test-theme-theme-persistence.el.disabled
+++ /dev/null
@@ -1,135 +0,0 @@
-;;; test-theme-theme-persistence.el --- Tests theme persistence mechanism -*- lexical-binding: t; -*-
-
-;;; Commentary:
-;; Unit tests for the persistence of the chosen theme
-
-;;; Code:
-
-(add-to-list 'load-path (concat user-emacs-directory "modules"))
-(require 'ui-theme)
-
-;; ------------------------ Constants / Setup / Teardown -----------------------
-
-(defvar cj/original-theme-name nil)
-(defvar cj/original-newline-setting nil)
-
-(defun cj/test-setup ()
- "Required settings and save state before each test."
-
- ;; save the current theme for restoration
- (setq cj/original-theme-name (symbol-name (car custom-enabled-themes)))
- (setq cj/original-newline-setting mode-require-final-newline)
-
- ;; unload all themes before starting test
- (mapcar #'disable-theme custom-enabled-themes)
-
- ;; no EOF newlines
- (custom-set-variables
- '(require-final-newline nil))
- (setq mode-require-final-newline nil))
-
-(defun cj/test-teardown ()
- "Restore the state before each test."
- ;; restore newline setting
- (setq require-final-newline cj/original-newline-setting)
-
- ;; if there wasn't an original theme, remove all themes
- (if (string= cj/original-theme-name "nil")
- (mapcar #'disable-theme custom-enabled-themes)
- ;; otherwise, restore it
- (load-theme (intern cj/original-theme-name))))
-
-;; ----------------------------------- Tests -----------------------------------
-
-(ert-deftest test-write-file-contents ()
- "Normal Case: Uses function to write a string, reads it back, and compares."
- (cj/test-setup)
- (let ((teststring "testing123")
- (testfilename "test-write-file-contents.txt"))
- ;; call the function
- (should (equal (cj/write-file-contents teststring testfilename)
- 't))
- ;; Read the file and check it's contents
- (should (equal (with-temp-buffer(insert-file-contents testfilename)
- (buffer-string))
- teststring))
- ;; clean up test file
- (delete-file testfilename))
- (cj/test-teardown))
-
-(ert-deftest test-write-file-not-writable ()
- "Test writing to a non-writable file."
- (cl-flet ((file-writeable-p (file) nil))
- (let* ((non-writable-file (make-temp-file "test-non-writable"))
- (should (equal (cj/write-file-contents "cowabunga" non-writable-file) 'nil)))
- (delete-file non-writable-file))))
-
-(ert-deftest test-read-file-contents ()
- "Normal Case: Writes string to file and reads contents using function."
- (cj/test-setup)
- (let ((teststring "testing123")
- (testfilename "test-read-file-contents.txt"))
- ;; write the file
- (with-temp-buffer
- (insert teststring)
- (write-file testfilename))
- ;; call the function
- (should (equal (cj/read-file-contents testfilename)
- teststring))
- ;; clean up test file
- (delete-file testfilename))
- (cj/test-teardown))
-
-(ert-deftest test-read-file-nonexistent ()
- "Test reading from a non-existent file returns nil."
- (cj/test-setup)
- (let* ((filename (concat (number-to-string (random 99999999)) "nonexistent-file.txt"))
- (result (cj/read-file-contents filename)))
- (should (equal result nil)))
- (cj/test-teardown))
-
-(ert-deftest test-get-active-theme ()
- (cj/test-setup)
- "Normal Case: Sets theme, gets theme-name, and compares."
- (let ((expected "wombat"))
- (load-theme (intern expected))
- (should (string= (cj/get-active-theme-name) expected))
- (cj/test-teardown)))
-
-(ert-deftest test-get-active-theme ()
- (cj/test-setup)
- "Normal Case: Sets theme, gets theme-name, and compares."
- (let ((expected "nil"))
- (mapcar #'disable-theme custom-enabled-themes)
- (should (equal (cj/get-active-theme-name) expected))
- (cj/test-teardown)))
-
-(ert-deftest test-save-theme-to-file ()
- "Normal case: sets theme, saves it, reads from file, and compares."
- (cj/test-setup)
- (let ((expected "wombat"))
- (load-theme (intern expected))
- (cj/save-theme-to-file)
- (should (equal (cj/read-file-contents theme-file) expected))
- (cj/test-teardown)))
-
-(ert-deftest test-load-theme-from-file ()
- "Normal case: saves new theme to file, loads it from file, and compares."
- (cj/test-setup)
- (let ((expected "wombat")) ;; the ui theme that test-setup uses.
- (cj/write-file-contents expected theme-file)
- (cj/load-theme-from-file)
- (should (equal expected (cj/get-active-theme-name))))
- (cj/test-teardown))
-
-(ert-deftest test-load-nil-theme ()
- "Corner case: saves 'nil as theme name to file, loads it, and compares to not having a theme."
- (cj/test-setup)
- (let ((expected "nil")) ;; the ui theme that test-setup uses.
- (cj/write-file-contents expected theme-file)
- (cj/load-theme-from-file)
- (should (equal expected (cj/get-active-theme-name))))
- (cj/test-teardown))
-
-(provide 'test-theme-theme-persistence)
-;;; test-theme-theme-persistence.el ends here.
diff --git a/tests/test-title-case-region.el.disabled b/tests/test-title-case-region.el.disabled
deleted file mode 100644
index ffab0c24..00000000
--- a/tests/test-title-case-region.el.disabled
+++ /dev/null
@@ -1,44 +0,0 @@
-;;; test-title-case-region.el --- -*- lexical-binding: t; -*-
-
-;;; Commentary:
-;; Tests for the title-case region function in custom-functions.el
-
-;; Note on Title Case
-;; Title case is a capitalization convention where major words are
-;; capitalized,and most minor words are lowercase. Nouns,verbs (including
-;; linking verbs), adjectives, adverbs,pronouns,and all words of four letters or
-;; more are considered major words. Short (i.e., three letters or fewer)
-;; conjunctions, short prepositions,and all articles are considered minor
-;; words."
-
-;; positive case (single line, all lowercase, no skip words)
-;; positive case (six lines, mixed case, skip words)
-;; negative case (single line, all skip-words)
-;; negative case (a long empty string)
-
-
-;;; Code:
-
-(require 'ert)
-(add-to-list 'load-path (concat user-emacs-directory "modules"))
-(require 'custom-functions)
-
-(ert-deftest test-cj/fixup-whitespace-positive-first-line-only ()
- "Test a positive case with two lines.
-Both lines have whitespace at the beginning and the end. This tests that when
-this function is called on the first line, only that line is affected."
- (let ((testdata " Hello, world! \n Foo bar ")
- (expected "Hello, world!\n Foo bar ")
- (actual))
- (with-temp-buffer
- (insert testdata)
- (goto-char (point-min))
- (cj/fixup-whitespace-line-or-region)
- (setq actual (buffer-string))
- (should (string= actual expected)))))
-
-
-
-
-(provide 'test-title-case-region)
-;;; test-title-case-region.el ends here.
diff --git a/tests/test-undead-buffers.el b/tests/test-undead-buffers.el
index 606972be..38187525 100644
--- a/tests/test-undead-buffers.el
+++ b/tests/test-undead-buffers.el
@@ -1,7 +1,16 @@
;;; test-undead-buffers.el --- -*- coding: utf-8; lexical-binding: t; -*-
;;; Commentary:
-;;
+;; ERT tests for undead-buffers.el.
+;; Exercises kill vs bury decisions driven by cj/buffer-bury-alive-list
+;; and window-management helpers.
+;; Coverage:
+;; - cj/kill-buffer-or-bury-alive: kills non-listed buffers; buries listed; C-u adds to list
+;; - cj/kill-buffer-and-window: deletes selected window, then kill/bury buffer as appropriate
+;; - cj/kill-other-window: deletes the other window, then kill/bury that buffer
+;; - cj/kill-all-other-buffers-and-windows: keeps only current window/buffer
+;; Tests isolate state with temporary buffers/windows and restore cj/buffer-bury-alive-list.
+;; Note: bury-buffer does not delete windows; tests assert buffer liveness, not window removal.
;;; Code:
diff --git a/tests/testutil-general.el b/tests/testutil-general.el
index d3c08135..b7222d1a 100644
--- a/tests/testutil-general.el
+++ b/tests/testutil-general.el
@@ -81,31 +81,6 @@ Return the full created path."
(cj/create--directory-ensuring-parents path)
(cj/create--file-ensuring-parents path content executable))))
-
-;; (defun cj/create-file-with-content-ensuring-parents (filepath content &optional executable)
-;; "Create a file at FILEPATH with CONTENT, ensuring parent directories exist.
-;; FILEPATH will be relative to `cj/test-base-dir'.
-;; Signals an error if the file already exists.
-;; If EXECUTABLE is non-nil, set executable permission on the file.
-;; Errors if the resulting path is outside `cj/test-base-dir`."
-;; (let* ((base (file-name-as-directory cj/test-base-dir))
-;; (fullpath (if (file-name-absolute-p filepath)
-;; (expand-file-name filepath)
-;; (expand-file-name filepath base))))
-;; (unless (string-prefix-p base fullpath)
-;; (error "File path %s is outside base test directory %s" fullpath base))
-;; (let ((parent-dir (file-name-directory fullpath)))
-;; (when (file-exists-p fullpath)
-;; (error "File already exists: %s" fullpath))
-;; (unless (file-directory-p parent-dir)
-;; (make-directory parent-dir t))
-;; (with-temp-buffer
-;; (insert content)
-;; (write-file fullpath))
-;; (when executable
-;; (chmod fullpath #o755))
-;; fullpath)))
-
(defun cj/fix-permissions-recursively (dir)
"Recursively set read/write permissions for user under DIR.
Directories get user read, write, and execute permissions to allow recursive