diff options
| -rw-r--r-- | modules/custom-misc.el | 90 | ||||
| -rw-r--r-- | tests/test-custom-misc-count-words.el | 148 | ||||
| -rw-r--r-- | tests/test-custom-misc-format-region.el | 161 | ||||
| -rw-r--r-- | tests/test-custom-misc-jump-to-matching-paren.el | 197 | ||||
| -rw-r--r-- | tests/test-custom-misc-replace-fraction-glyphs.el | 185 |
5 files changed, 750 insertions, 31 deletions
diff --git a/modules/custom-misc.el b/modules/custom-misc.el index 0c6d7ac8..2af9c244 100644 --- a/modules/custom-misc.el +++ b/modules/custom-misc.el @@ -46,19 +46,27 @@ If not on a delimiter, show a message. Respects the current syntax table." (message "Point is not on a delimiter."))))) +(defun cj/--format-region (start end) + "Internal implementation: Reformat text between START and END. +START and END define the region to operate on. +Replaces tabs with spaces, reindents, and deletes trailing whitespace." + (when (> start end) + (error "Invalid region: start (%d) is greater than end (%d)" start end)) + (save-excursion + (save-restriction + (narrow-to-region start end) + (untabify (point-min) (point-max)) + (indent-region (point-min) (point-max)) + (delete-trailing-whitespace (point-min) (point-max))))) + (defun cj/format-region-or-buffer () "Reformat the region or the entire buffer. Replaces tabs with spaces, deletes trailing whitespace, and reindents." (interactive) (let ((start-pos (if (use-region-p) (region-beginning) (point-min))) - (end-pos (if (use-region-p) (region-end) (point-max)))) - (save-excursion - (save-restriction - (narrow-to-region start-pos end-pos) - (untabify (point-min) (point-max)) - (indent-region (point-min) (point-max)) - (delete-trailing-whitespace (point-min) (point-max)))) - (message "Formatted %s" (if (use-region-p) "region" "buffer")))) + (end-pos (if (use-region-p) (region-end) (point-max)))) + (cj/--format-region start-pos end-pos) + (message "Formatted %s" (if (use-region-p) "region" "buffer")))) (defun cj/switch-to-previous-buffer () "Switch to previously open buffer. @@ -66,6 +74,14 @@ Repeated invocations toggle between the two most recently open buffers." (interactive) (switch-to-buffer (other-buffer (current-buffer) 1))) +(defun cj/--count-words (start end) + "Internal implementation: Count words between START and END. +START and END define the region to count. +Returns the word count as an integer." + (when (> start end) + (error "Invalid region: start (%d) is greater than end (%d)" start end)) + (count-words start end)) + (defun cj/count-words-buffer-or-region () "Count the number of words in the buffer or region. Display the result in the minibuffer." @@ -73,9 +89,38 @@ Display the result in the minibuffer." (let* ((use-region (use-region-p)) (begin (if use-region (region-beginning) (point-min))) (end (if use-region (region-end) (point-max))) - (area-type (if use-region "the region" "the buffer"))) - (message "There are %d words in %s." (count-words begin end) area-type))) + (area-type (if use-region "the region" "the buffer")) + (word-count (cj/--count-words begin end))) + (message "There are %d words in %s." word-count area-type))) + +(defun cj/--replace-fraction-glyphs (start end to-glyphs) + "Internal implementation: Replace fraction glyphs or text between START and END. +START and END define the region to operate on. +TO-GLYPHS when non-nil converts text (1/4) to glyphs (¼), +otherwise converts glyphs to text." + (when (> start end) + (error "Invalid region: start (%d) is greater than end (%d)" start end)) + (let ((replacements (if to-glyphs + '(("1/4" . "¼") + ("1/2" . "½") + ("3/4" . "¾") + ("1/3" . "⅓") + ("2/3" . "⅔")) + '(("¼" . "1/4") + ("½" . "1/2") + ("¾" . "3/4") + ("⅓" . "1/3") + ("⅔" . "2/3")))) + (count 0) + (end-marker (copy-marker end))) + (save-excursion + (dolist (r replacements) + (goto-char start) + (while (search-forward (car r) end-marker t) + (replace-match (cdr r)) + (setq count (1+ count))))) + count)) (defun cj/replace-fraction-glyphs (start end) "Replace common fraction glyphs between START and END. @@ -83,27 +128,10 @@ Operate on the buffer or region designated by START and END. Replace the text representations with glyphs when called with a \\[universal-argument] prefix." (interactive (if (use-region-p) - (list (region-beginning) (region-end)) - (list (point-min) (point-max)))) - (let ((replacements (if current-prefix-arg - '(("1/4" . "¼") - ("1/2" . "½") - ("3/4" . "¾") - ("1/3" . "⅓") - ("2/3" . "⅔")) - '(("¼" . "1/4") - ("½" . "1/2") - ("¾" . "3/4") - ("⅓" . "1/3") - ("⅔" . "2/3")))) - (count 0)) - (save-excursion - (dolist (r replacements) - (goto-char start) - (while (search-forward (car r) end t) - (replace-match (cdr r)) - (setq count (1+ count))))) - (message "Replaced %d fraction%s" count (if (= count 1) "" "s")))) + (list (region-beginning) (region-end)) + (list (point-min) (point-max)))) + (let ((count (cj/--replace-fraction-glyphs start end current-prefix-arg))) + (message "Replaced %d fraction%s" count (if (= count 1) "" "s")))) (defun cj/align-regexp-with-spaces (orig-fun &rest args) "Call ORIG-FUN with ARGS while temporarily disabling tabs for alignment. 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 |
