diff options
| -rw-r--r-- | modules/custom-text-enclose.el | 295 | ||||
| -rw-r--r-- | tests/test-custom-text-enclose-append.el | 190 | ||||
| -rw-r--r-- | tests/test-custom-text-enclose-indent.el | 241 | ||||
| -rw-r--r-- | tests/test-custom-text-enclose-prepend.el | 207 | ||||
| -rw-r--r-- | tests/test-custom-text-enclose-surround.el | 200 | ||||
| -rw-r--r-- | tests/test-custom-text-enclose-unwrap.el | 266 | ||||
| -rw-r--r-- | tests/test-custom-text-enclose-wrap.el | 240 |
7 files changed, 1587 insertions, 52 deletions
diff --git a/modules/custom-text-enclose.el b/modules/custom-text-enclose.el index 514419cd..ccacdd2d 100644 --- a/modules/custom-text-enclose.el +++ b/modules/custom-text-enclose.el @@ -2,78 +2,269 @@ ;;; Commentary: -;; This module provides functions to surround words or regions with custom strings, and to append or prepend text to lines. +;; Text enclosure utilities for wrapping and line manipulation. +;; +;; Wrapping functions: +;; - surround-word-or-region - wrap text with same delimiter on both sides +;; - wrap-word-or-region - wrap with different opening/closing delimiters +;; - unwrap-word-or-region - remove surrounding delimiters +;; +;; Line manipulation: +;; - append-to-lines - add suffix to each line +;; - prepend-to-lines - add prefix to each line +;; - indent-lines - add leading whitespace (spaces or tabs) +;; - dedent-lines - remove leading whitespace +;; +;; Most functions work on region or entire buffer when no region is active. +;; +;; Bound to keymap prefix C-; s -;; It includes three main functions: -;; - surround word or region with a user-specified string -;; - append text to the end of lines -;; - prepend text to the beginning of lines +;;; Code: -;; All functions work on both the active region and the entire buffer when no region is selected. +;; cj/custom-keymap defined in keybindings.el +(eval-when-compile (defvar cj/custom-keymap)) -;; Bound to keymap prefix C-; s +(defun cj/--surround (text surround-string) + "Internal implementation: Surround TEXT with SURROUND-STRING. +TEXT is the string to be surrounded. +SURROUND-STRING is prepended and appended to TEXT. +Returns the surrounded text without modifying the buffer." + (concat surround-string text surround-string)) -;;; Code: +(defun cj/--wrap (text opening closing) + "Internal implementation: Wrap TEXT with OPENING and CLOSING strings. +TEXT is the string to be wrapped. +OPENING is prepended to TEXT. +CLOSING is appended to TEXT. +Returns the wrapped text without modifying the buffer." + (concat opening text closing)) (defun cj/surround-word-or-region () - "Surround the word at point or active region with a string read from the minibuffer." + "Surround the word at point or active region with a string. +The surround string is read from the minibuffer." (interactive) (let ((str (read-string "Surround with: ")) (regionp (use-region-p))) - (save-excursion - (if regionp - (let ((beg (region-beginning)) - (end (region-end))) - (goto-char end) - (insert str) - (goto-char beg) - (insert str)) - (if (thing-at-point 'word) - (let ((bounds (bounds-of-thing-at-point 'word))) - (goto-char (cdr bounds)) - (insert str) - (goto-char (car bounds)) - (insert str)) - (message "Can't insert around. No word at point and no region selected.")))))) + (if regionp + (let ((beg (region-beginning)) + (end (region-end)) + (text (buffer-substring (region-beginning) (region-end)))) + (delete-region beg end) + (goto-char beg) + (insert (cj/--surround text str))) + (if (thing-at-point 'word) + (let* ((bounds (bounds-of-thing-at-point 'word)) + (text (buffer-substring (car bounds) (cdr bounds)))) + (delete-region (car bounds) (cdr bounds)) + (goto-char (car bounds)) + (insert (cj/--surround text str))) + (message "Can't insert around. No word at point and no region selected."))))) + +(defun cj/wrap-word-or-region () + "Wrap the word at point or active region with different opening/closing strings. +The opening and closing strings are read from the minibuffer." + (interactive) + (let ((opening (read-string "Opening: ")) + (closing (read-string "Closing: ")) + (regionp (use-region-p))) + (if regionp + (let ((beg (region-beginning)) + (end (region-end)) + (text (buffer-substring (region-beginning) (region-end)))) + (delete-region beg end) + (goto-char beg) + (insert (cj/--wrap text opening closing))) + (if (thing-at-point 'word) + (let* ((bounds (bounds-of-thing-at-point 'word)) + (text (buffer-substring (car bounds) (cdr bounds)))) + (delete-region (car bounds) (cdr bounds)) + (goto-char (car bounds)) + (insert (cj/--wrap text opening closing))) + (message "Can't wrap. No word at point and no region selected."))))) + +(defun cj/--unwrap (text opening closing) + "Internal implementation: Remove OPENING and CLOSING from TEXT if present. +TEXT is the string to unwrap. +OPENING is checked at the start of TEXT. +CLOSING is checked at the end of TEXT. +Returns the unwrapped text if both delimiters present, otherwise unchanged." + (if (and (string-prefix-p opening text) + (string-suffix-p closing text) + (>= (length text) (+ (length opening) (length closing)))) + (substring text (length opening) (- (length text) (length closing))) + text)) + +(defun cj/unwrap-word-or-region () + "Remove surrounding delimiters from word at point or active region. +The opening and closing strings are read from the minibuffer." + (interactive) + (let ((opening (read-string "Opening to remove: ")) + (closing (read-string "Closing to remove: ")) + (regionp (use-region-p))) + (if regionp + (let ((beg (region-beginning)) + (end (region-end)) + (text (buffer-substring (region-beginning) (region-end)))) + (delete-region beg end) + (goto-char beg) + (insert (cj/--unwrap text opening closing))) + (if (thing-at-point 'word) + (let* ((bounds (bounds-of-thing-at-point 'word)) + (text (buffer-substring (car bounds) (cdr bounds)))) + (delete-region (car bounds) (cdr bounds)) + (goto-char (car bounds)) + (insert (cj/--unwrap text opening closing))) + (message "Can't unwrap. No word at point and no region selected."))))) + +(defun cj/--append-to-lines (text suffix) + "Internal implementation: Append SUFFIX to each line in TEXT. +TEXT is the string containing one or more lines. +SUFFIX is appended to the end of each line. +Returns the transformed string without modifying the buffer." + (let* ((lines (split-string text "\n")) + (has-trailing-newline (string-suffix-p "\n" text)) + ;; If has trailing newline, last element will be empty string - exclude it + (lines-to-process (if (and has-trailing-newline + (not (null lines)) + (string-empty-p (car (last lines)))) + (butlast lines) + lines))) + (concat + (mapconcat (lambda (line) (concat line suffix)) lines-to-process "\n") + (if has-trailing-newline "\n" "")))) (defun cj/append-to-lines-in-region-or-buffer (str) "Append STR to the end of each line in the region or entire buffer." (interactive "sEnter string to append: ") - (let ((start-pos (if (use-region-p) - (region-beginning) - (point-min))) - (end-pos (if (use-region-p) - (region-end) - (point-max)))) - (save-excursion - (goto-char start-pos) - (while (< (point) end-pos) - (move-end-of-line 1) - (insert str) - (forward-line 1))))) + (let* ((start-pos (if (use-region-p) + (region-beginning) + (point-min))) + (end-pos (if (use-region-p) + (region-end) + (point-max))) + (text (buffer-substring start-pos end-pos)) + (insertion (cj/--append-to-lines text str))) + (delete-region start-pos end-pos) + (goto-char start-pos) + (insert insertion))) + +(defun cj/--prepend-to-lines (text prefix) + "Internal implementation: Prepend PREFIX to each line in TEXT. +TEXT is the string containing one or more lines. +PREFIX is prepended to the beginning of each line. +Returns the transformed string without modifying the buffer." + (let* ((lines (split-string text "\n")) + (has-trailing-newline (string-suffix-p "\n" text)) + ;; If has trailing newline, last element will be empty string - exclude it + (lines-to-process (if (and has-trailing-newline + (not (null lines)) + (string-empty-p (car (last lines)))) + (butlast lines) + lines))) + (concat + (mapconcat (lambda (line) (concat prefix line)) lines-to-process "\n") + (if has-trailing-newline "\n" "")))) (defun cj/prepend-to-lines-in-region-or-buffer (str) "Prepend STR to the beginning of each line in the region or entire buffer." (interactive "sEnter string to prepend: ") - (let ((start-pos (if (use-region-p) - (region-beginning) - (point-min))) - (end-pos (if (use-region-p) - (region-end) - (point-max)))) - (save-excursion - (goto-char start-pos) - (while (< (point) end-pos) - (beginning-of-line 1) - (insert str) - (forward-line 1))))) - -;; Surround, append, prepend prefix keymap + (let* ((start-pos (if (use-region-p) + (region-beginning) + (point-min))) + (end-pos (if (use-region-p) + (region-end) + (point-max))) + (text (buffer-substring start-pos end-pos)) + (insertion (cj/--prepend-to-lines text str))) + (delete-region start-pos end-pos) + (goto-char start-pos) + (insert insertion))) + +(defun cj/--indent-lines (text count use-tabs) + "Internal implementation: Indent each line in TEXT by COUNT characters. +TEXT is the string containing one or more lines. +COUNT is the number of indentation characters to add. +USE-TABS when non-nil uses tabs instead of spaces for indentation. +Returns the indented text without modifying the buffer." + (let ((indent-string (if use-tabs + (make-string count ?\t) + (make-string count ?\s)))) + (cj/--prepend-to-lines text indent-string))) + +(defun cj/indent-lines-in-region-or-buffer (count use-tabs) + "Indent each line in region or buffer by COUNT characters. +COUNT is the number of characters to indent (default 4). +USE-TABS when non-nil (prefix argument) uses tabs instead of spaces." + (interactive "p\nP") + (let* ((start-pos (if (use-region-p) + (region-beginning) + (point-min))) + (end-pos (if (use-region-p) + (region-end) + (point-max))) + (text (buffer-substring start-pos end-pos)) + (insertion (cj/--indent-lines text count use-tabs))) + (delete-region start-pos end-pos) + (goto-char start-pos) + (insert insertion))) + +(defun cj/--dedent-lines (text count) + "Internal implementation: Remove up to COUNT leading characters from each line. +TEXT is the string containing one or more lines. +COUNT is the maximum number of leading whitespace characters to remove. +Removes spaces and tabs, but only up to COUNT characters per line. +Returns the dedented text without modifying the buffer." + (let* ((lines (split-string text "\n")) + (has-trailing-newline (string-suffix-p "\n" text)) + (lines-to-process (if (and has-trailing-newline + (not (null lines)) + (string-empty-p (car (last lines)))) + (butlast lines) + lines)) + (dedented-lines + (mapcar + (lambda (line) + (let ((removed 0) + (pos 0) + (len (length line))) + (while (and (< removed count) + (< pos len) + (memq (aref line pos) '(?\s ?\t))) + (setq removed (1+ removed)) + (setq pos (1+ pos))) + (substring line pos))) + lines-to-process))) + (concat + (mapconcat #'identity dedented-lines "\n") + (if has-trailing-newline "\n" "")))) + +(defun cj/dedent-lines-in-region-or-buffer (count) + "Remove up to COUNT leading whitespace characters from each line. +COUNT is the number of characters to remove (default 4). +Works on region if active, otherwise entire buffer." + (interactive "p") + (let* ((start-pos (if (use-region-p) + (region-beginning) + (point-min))) + (end-pos (if (use-region-p) + (region-end) + (point-max))) + (text (buffer-substring start-pos end-pos)) + (insertion (cj/--dedent-lines text count))) + (delete-region start-pos end-pos) + (goto-char start-pos) + (insert insertion))) + +;; Text enclosure keymap (defvar-keymap cj/enclose-map - :doc "Keymap for enclosing text: surrounding, appending, and prepending" + :doc "Keymap for text enclosure: wrapping, line manipulation, and indentation" "s" #'cj/surround-word-or-region + "w" #'cj/wrap-word-or-region + "u" #'cj/unwrap-word-or-region "a" #'cj/append-to-lines-in-region-or-buffer - "p" #'cj/prepend-to-lines-in-region-or-buffer) + "p" #'cj/prepend-to-lines-in-region-or-buffer + "i" #'cj/indent-lines-in-region-or-buffer + "d" #'cj/dedent-lines-in-region-or-buffer) (keymap-set cj/custom-keymap "s" cj/enclose-map) (with-eval-after-load 'which-key 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 |
