diff options
Diffstat (limited to 'tests')
57 files changed, 9804 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-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 |
