diff options
| author | Craig Jennings <c@cjennings.net> | 2026-02-14 00:45:55 -0600 |
|---|---|---|
| committer | Craig Jennings <c@cjennings.net> | 2026-02-14 00:45:55 -0600 |
| commit | ebd8b2ce83941386b196e663d8f8ba83d7ce44c1 (patch) | |
| tree | dba9b2da35a60c44f039495fbab565a0500c4373 /tests | |
| parent | 78c3ef3c2008f72f9e46f30447c68d627bd693cd (diff) | |
test: add ERT coverage for modeline-config and hugo-config
Add 67 tests across 6 new test files for modeline and hugo modules.
Refactor hugo-config: extract post-file-path and post-template helpers
from interactive new-post function for testability. Update todo.org
with test audit (31 modules), priority adjustments, and task cleanup.
Diffstat (limited to 'tests')
| -rw-r--r-- | tests/test-hugo-config-open-blog-dir-external.el | 105 | ||||
| -rw-r--r-- | tests/test-hugo-config-post-file-path.el | 99 | ||||
| -rw-r--r-- | tests/test-hugo-config-post-template.el | 120 | ||||
| -rw-r--r-- | tests/test-hugo-config-toggle-draft.el | 161 | ||||
| -rw-r--r-- | tests/test-modeline-config-string-cut-middle.el | 145 | ||||
| -rw-r--r-- | tests/test-modeline-config-string-truncate-p.el | 132 |
6 files changed, 762 insertions, 0 deletions
diff --git a/tests/test-hugo-config-open-blog-dir-external.el b/tests/test-hugo-config-open-blog-dir-external.el new file mode 100644 index 00000000..ae4a25ba --- /dev/null +++ b/tests/test-hugo-config-open-blog-dir-external.el @@ -0,0 +1,105 @@ +;;; test-hugo-config-open-blog-dir-external.el --- Tests for cj/hugo-open-blog-dir-external -*- lexical-binding: t; -*- + +;;; Commentary: +;; Tests for the cj/hugo-open-blog-dir-external function from hugo-config.el +;; +;; This function opens the blog source directory in the system file manager, +;; selecting the command based on platform: +;; - macOS: "open" +;; - Windows: "explorer.exe" +;; - Linux/other: "xdg-open" +;; +;; We mock the platform detection functions and start-process to verify +;; the correct command is dispatched per platform. + +;;; Code: + +(require 'ert) + +;; Add modules directory to load path +(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory)) + +;; Stub dependencies before loading the module +(unless (boundp 'website-dir) + (defvar website-dir "/tmp/test-website/")) +(unless (fboundp 'env-macos-p) + (defun env-macos-p () nil)) +(unless (fboundp 'env-windows-p) + (defun env-windows-p () nil)) + +(require 'hugo-config) + +;;; Test Helpers + +(defvar test-hugo--captured-process-cmd nil + "Captures the command passed to start-process during tests.") + +(defmacro with-platform (macos-p windows-p &rest body) + "Execute BODY with mocked platform detection. +MACOS-P and WINDOWS-P control env-macos-p and env-windows-p return values. +Mocks start-process to capture the command and file-directory-p to avoid +filesystem checks." + (declare (indent 2)) + `(let ((test-hugo--captured-process-cmd nil)) + (cl-letf (((symbol-function 'env-macos-p) (lambda () ,macos-p)) + ((symbol-function 'env-windows-p) (lambda () ,windows-p)) + ((symbol-function 'file-directory-p) (lambda (_d) t)) + ((symbol-function 'start-process) + (lambda (_name _buf cmd &rest _args) + (setq test-hugo--captured-process-cmd cmd)))) + ,@body))) + +;;; Normal Cases + +(ert-deftest test-hugo-config-open-blog-dir-external-normal-linux () + "Should use xdg-open on Linux." + (with-platform nil nil + (cj/hugo-open-blog-dir-external) + (should (string= test-hugo--captured-process-cmd "xdg-open")))) + +(ert-deftest test-hugo-config-open-blog-dir-external-normal-macos () + "Should use open on macOS." + (with-platform t nil + (cj/hugo-open-blog-dir-external) + (should (string= test-hugo--captured-process-cmd "open")))) + +(ert-deftest test-hugo-config-open-blog-dir-external-normal-windows () + "Should use explorer.exe on Windows." + (with-platform nil t + (cj/hugo-open-blog-dir-external) + (should (string= test-hugo--captured-process-cmd "explorer.exe")))) + +;;; Boundary Cases + +(ert-deftest test-hugo-config-open-blog-dir-external-boundary-macos-takes-precedence () + "When both macos and windows return true, macOS should take precedence." + (with-platform t t + (cj/hugo-open-blog-dir-external) + (should (string= test-hugo--captured-process-cmd "open")))) + +(ert-deftest test-hugo-config-open-blog-dir-external-boundary-creates-missing-dir () + "Should create the directory if it doesn't exist." + (let ((mkdir-called nil)) + (cl-letf (((symbol-function 'env-macos-p) (lambda () nil)) + ((symbol-function 'env-windows-p) (lambda () nil)) + ((symbol-function 'file-directory-p) (lambda (_d) nil)) + ((symbol-function 'make-directory) + (lambda (_dir &rest _args) (setq mkdir-called t))) + ((symbol-function 'start-process) #'ignore)) + (cj/hugo-open-blog-dir-external) + (should mkdir-called)))) + +(ert-deftest test-hugo-config-open-blog-dir-external-boundary-skips-mkdir-when-exists () + "Should not call make-directory if directory already exists." + (let ((mkdir-called nil)) + (cl-letf (((symbol-function 'env-macos-p) (lambda () nil)) + ((symbol-function 'env-windows-p) (lambda () nil)) + ((symbol-function 'file-directory-p) (lambda (_d) t)) + ((symbol-function 'make-directory) + (lambda (_dir &rest _args) (setq mkdir-called t))) + ((symbol-function 'start-process) #'ignore)) + (cj/hugo-open-blog-dir-external) + (should-not mkdir-called)))) + +(provide 'test-hugo-config-open-blog-dir-external) +;;; test-hugo-config-open-blog-dir-external.el ends here diff --git a/tests/test-hugo-config-post-file-path.el b/tests/test-hugo-config-post-file-path.el new file mode 100644 index 00000000..ac6bc692 --- /dev/null +++ b/tests/test-hugo-config-post-file-path.el @@ -0,0 +1,99 @@ +;;; test-hugo-config-post-file-path.el --- Tests for cj/hugo--post-file-path -*- lexical-binding: t; -*- + +;;; Commentary: +;; Tests for the cj/hugo--post-file-path function from hugo-config.el +;; +;; This function takes a post title, generates a slug via org-hugo-slug, +;; and returns the full file path under cj/hugo-content-org-dir. +;; +;; We mock org-hugo-slug to isolate our path construction logic from +;; the ox-hugo package (external dependency). + +;;; Code: + +(require 'ert) + +;; Add modules directory to load path +(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory)) + +;; Stub dependencies before loading the module +(unless (boundp 'website-dir) + (defvar website-dir "/tmp/test-website/")) +(unless (fboundp 'env-macos-p) + (defun env-macos-p () nil)) +(unless (fboundp 'env-windows-p) + (defun env-windows-p () nil)) +(unless (fboundp 'org-hugo-slug) + (defun org-hugo-slug (title) title)) + +;; Stub ox-hugo require since MELPA packages aren't available in batch +(provide 'ox-hugo) + +(require 'hugo-config) + +;;; Normal Cases + +(ert-deftest test-hugo-config-post-file-path-normal-simple-title () + "Should construct path from slugified title." + (cl-letf (((symbol-function 'org-hugo-slug) + (lambda (title) "my-first-post"))) + (let ((result (cj/hugo--post-file-path "My First Post"))) + (should (string-suffix-p "my-first-post.org" result))))) + +(ert-deftest test-hugo-config-post-file-path-normal-under-content-org-dir () + "Should place the file under cj/hugo-content-org-dir." + (cl-letf (((symbol-function 'org-hugo-slug) + (lambda (title) "test-post"))) + (let ((result (cj/hugo--post-file-path "Test Post"))) + (should (string-prefix-p (expand-file-name cj/hugo-content-org-dir) result))))) + +(ert-deftest test-hugo-config-post-file-path-normal-org-extension () + "Should always produce a .org file." + (cl-letf (((symbol-function 'org-hugo-slug) + (lambda (title) "any-slug"))) + (let ((result (cj/hugo--post-file-path "Any Title"))) + (should (string-suffix-p ".org" result))))) + +(ert-deftest test-hugo-config-post-file-path-normal-different-titles () + "Different titles should produce different file paths." + (cl-letf (((symbol-function 'org-hugo-slug) + (lambda (title) + (if (string= title "Alpha") "alpha" "beta")))) + (let ((path-a (cj/hugo--post-file-path "Alpha")) + (path-b (cj/hugo--post-file-path "Beta"))) + (should-not (string= path-a path-b))))) + +;;; Boundary Cases + +(ert-deftest test-hugo-config-post-file-path-boundary-title-with-special-chars () + "Should handle titles with special characters via slug." + (cl-letf (((symbol-function 'org-hugo-slug) + (lambda (title) "whats-new-in-2026"))) + (let ((result (cj/hugo--post-file-path "What's New in 2026?!"))) + (should (string-suffix-p "whats-new-in-2026.org" result))))) + +(ert-deftest test-hugo-config-post-file-path-boundary-single-word-title () + "Should handle single word title." + (cl-letf (((symbol-function 'org-hugo-slug) + (lambda (title) "hello"))) + (let ((result (cj/hugo--post-file-path "Hello"))) + (should (string-suffix-p "hello.org" result))))) + +(ert-deftest test-hugo-config-post-file-path-boundary-very-long-title () + "Should handle a very long title." + (cl-letf (((symbol-function 'org-hugo-slug) + (lambda (title) "this-is-a-very-long-title-that-goes-on-and-on"))) + (let ((result (cj/hugo--post-file-path "This Is A Very Long Title That Goes On And On"))) + (should (string-suffix-p "this-is-a-very-long-title-that-goes-on-and-on.org" result))))) + +;;; Error Cases + +(ert-deftest test-hugo-config-post-file-path-error-empty-title () + "Empty title should still produce a valid path (slug handles it)." + (cl-letf (((symbol-function 'org-hugo-slug) + (lambda (title) ""))) + (let ((result (cj/hugo--post-file-path ""))) + (should (string-suffix-p ".org" result))))) + +(provide 'test-hugo-config-post-file-path) +;;; test-hugo-config-post-file-path.el ends here diff --git a/tests/test-hugo-config-post-template.el b/tests/test-hugo-config-post-template.el new file mode 100644 index 00000000..a464c30b --- /dev/null +++ b/tests/test-hugo-config-post-template.el @@ -0,0 +1,120 @@ +;;; test-hugo-config-post-template.el --- Tests for cj/hugo--post-template -*- lexical-binding: t; -*- + +;;; Commentary: +;; Tests for the cj/hugo--post-template function from hugo-config.el +;; +;; This function generates the Org front matter template for a Hugo post. +;; It takes a title and date, and returns the complete template string +;; with all required Hugo keywords. + +;;; Code: + +(require 'ert) + +;; Add modules directory to load path +(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory)) + +;; Stub dependencies before loading the module +(unless (boundp 'website-dir) + (defvar website-dir "/tmp/test-website/")) +(unless (fboundp 'env-macos-p) + (defun env-macos-p () nil)) +(unless (fboundp 'env-windows-p) + (defun env-windows-p () nil)) + +(require 'hugo-config) + +;;; Normal Cases + +(ert-deftest test-hugo-config-post-template-normal-contains-title () + "Template should contain the provided title." + (let ((result (cj/hugo--post-template "My First Post" "2026-02-14"))) + (should (string-match-p "#\\+title: My First Post" result)))) + +(ert-deftest test-hugo-config-post-template-normal-contains-date () + "Template should contain the provided date." + (let ((result (cj/hugo--post-template "Test" "2026-02-14"))) + (should (string-match-p "#\\+date: 2026-02-14" result)))) + +(ert-deftest test-hugo-config-post-template-normal-contains-base-dir () + "Template should contain hugo_base_dir pointing to site root." + (let ((result (cj/hugo--post-template "Test" "2026-02-14"))) + (should (string-match-p "#\\+hugo_base_dir: \\.\\./\\.\\." result)))) + +(ert-deftest test-hugo-config-post-template-normal-contains-section () + "Template should set hugo_section to log." + (let ((result (cj/hugo--post-template "Test" "2026-02-14"))) + (should (string-match-p "#\\+hugo_section: log" result)))) + +(ert-deftest test-hugo-config-post-template-normal-draft-true () + "New posts should default to draft: true." + (let ((result (cj/hugo--post-template "Test" "2026-02-14"))) + (should (string-match-p "#\\+hugo_draft: true" result)))) + +(ert-deftest test-hugo-config-post-template-normal-has-lastmod () + "Template should enable auto-set lastmod." + (let ((result (cj/hugo--post-template "Test" "2026-02-14"))) + (should (string-match-p "#\\+hugo_auto_set_lastmod: t" result)))) + +(ert-deftest test-hugo-config-post-template-normal-has-tags-placeholder () + "Template should include empty hugo_tags keyword." + (let ((result (cj/hugo--post-template "Test" "2026-02-14"))) + (should (string-match-p "#\\+hugo_tags:" result)))) + +(ert-deftest test-hugo-config-post-template-normal-has-description () + "Template should include description custom front matter." + (let ((result (cj/hugo--post-template "Test" "2026-02-14"))) + (should (string-match-p "#\\+hugo_custom_front_matter: :description" result)))) + +(ert-deftest test-hugo-config-post-template-normal-ends-with-blank-line () + "Template should end with a blank line for content insertion." + (let ((result (cj/hugo--post-template "Test" "2026-02-14"))) + (should (string-suffix-p "\n\n" result)))) + +;;; Boundary Cases + +(ert-deftest test-hugo-config-post-template-boundary-title-with-quotes () + "Title containing quotes should be inserted literally." + (let ((result (cj/hugo--post-template "It's a \"Test\"" "2026-02-14"))) + (should (string-match-p "#\\+title: It's a \"Test\"" result)))) + +(ert-deftest test-hugo-config-post-template-boundary-title-with-colons () + "Title with colons should be inserted literally." + (let ((result (cj/hugo--post-template "Part 1: The Beginning" "2026-02-14"))) + (should (string-match-p "#\\+title: Part 1: The Beginning" result)))) + +(ert-deftest test-hugo-config-post-template-boundary-empty-title () + "Empty title should produce valid template with empty title line." + (let ((result (cj/hugo--post-template "" "2026-02-14"))) + (should (string-match-p "#\\+title: \n" result)) + (should (string-match-p "#\\+date: 2026-02-14" result)))) + +(ert-deftest test-hugo-config-post-template-boundary-keyword-order () + "Keywords should appear in the expected order." + (let* ((result (cj/hugo--post-template "Test" "2026-02-14")) + (pos-base (string-match "#\\+hugo_base_dir" result)) + (pos-section (string-match "#\\+hugo_section" result)) + (pos-title (string-match "#\\+title" result)) + (pos-date (string-match "#\\+date" result)) + (pos-draft (string-match "#\\+hugo_draft" result))) + (should (< pos-base pos-section)) + (should (< pos-section pos-title)) + (should (< pos-title pos-date)) + (should (< pos-date pos-draft)))) + +;;; Error Cases + +(ert-deftest test-hugo-config-post-template-error-nil-title () + "Nil title should produce template with 'nil' string (format behavior)." + (let ((result (cj/hugo--post-template nil "2026-02-14"))) + (should (stringp result)) + (should (string-match-p "#\\+date: 2026-02-14" result)))) + +(ert-deftest test-hugo-config-post-template-error-nil-date () + "Nil date should produce template with 'nil' string (format behavior)." + (let ((result (cj/hugo--post-template "Test" nil))) + (should (stringp result)) + (should (string-match-p "#\\+title: Test" result)))) + +(provide 'test-hugo-config-post-template) +;;; test-hugo-config-post-template.el ends here diff --git a/tests/test-hugo-config-toggle-draft.el b/tests/test-hugo-config-toggle-draft.el new file mode 100644 index 00000000..2e75a7c9 --- /dev/null +++ b/tests/test-hugo-config-toggle-draft.el @@ -0,0 +1,161 @@ +;;; test-hugo-config-toggle-draft.el --- Tests for cj/hugo-toggle-draft -*- lexical-binding: t; -*- + +;;; Commentary: +;; Tests for the cj/hugo-toggle-draft function from hugo-config.el +;; +;; This function toggles the #+hugo_draft keyword between "true" and "false" +;; in the current buffer, using regex search and replace-match. +;; +;; We test the buffer manipulation logic directly using temp buffers, +;; mocking only save-buffer to avoid file I/O. + +;;; Code: + +(require 'ert) + +;; Add modules directory to load path +(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory)) + +;; Stub dependencies before loading the module +(unless (boundp 'website-dir) + (defvar website-dir "/tmp/test-website/")) +(unless (fboundp 'env-macos-p) + (defun env-macos-p () nil)) +(unless (fboundp 'env-windows-p) + (defun env-windows-p () nil)) + +(require 'hugo-config) + +;;; Test Helpers + +(defun test-hugo-toggle-draft-in-buffer (content) + "Run cj/hugo-toggle-draft on buffer with CONTENT. +Returns the buffer string after toggling. Mocks save-buffer." + (with-temp-buffer + (insert content) + (cl-letf (((symbol-function 'save-buffer) #'ignore)) + (cj/hugo-toggle-draft)) + (buffer-string))) + +(defconst test-hugo-draft-true-post + "#+hugo_base_dir: ../../ +#+hugo_section: log +#+hugo_auto_set_lastmod: t +#+title: Test Post +#+date: 2026-02-14 +#+hugo_tags: test +#+hugo_draft: true +#+hugo_custom_front_matter: :description \"A test post.\" + +Some content here. +" + "Sample post with draft set to true.") + +(defconst test-hugo-draft-false-post + "#+hugo_base_dir: ../../ +#+hugo_section: log +#+hugo_auto_set_lastmod: t +#+title: Test Post +#+date: 2026-02-14 +#+hugo_tags: test +#+hugo_draft: false +#+hugo_custom_front_matter: :description \"A test post.\" + +Some content here. +" + "Sample post with draft set to false.") + +;;; Normal Cases + +(ert-deftest test-hugo-config-toggle-draft-normal-true-to-false () + "Should toggle draft from true to false." + (let ((result (test-hugo-toggle-draft-in-buffer test-hugo-draft-true-post))) + (should (string-match-p "#\\+hugo_draft: false" result)) + (should-not (string-match-p "#\\+hugo_draft: true" result)))) + +(ert-deftest test-hugo-config-toggle-draft-normal-false-to-true () + "Should toggle draft from false to true." + (let ((result (test-hugo-toggle-draft-in-buffer test-hugo-draft-false-post))) + (should (string-match-p "#\\+hugo_draft: true" result)) + (should-not (string-match-p "#\\+hugo_draft: false" result)))) + +(ert-deftest test-hugo-config-toggle-draft-normal-preserves-other-content () + "Should not modify any other lines in the buffer." + (let ((result (test-hugo-toggle-draft-in-buffer test-hugo-draft-true-post))) + (should (string-match-p "#\\+title: Test Post" result)) + (should (string-match-p "#\\+hugo_tags: test" result)) + (should (string-match-p "Some content here\\." result)))) + +(ert-deftest test-hugo-config-toggle-draft-normal-roundtrip () + "Toggling twice should return to original state." + (let* ((first (test-hugo-toggle-draft-in-buffer test-hugo-draft-true-post)) + (second (with-temp-buffer + (insert first) + (cl-letf (((symbol-function 'save-buffer) #'ignore)) + (cj/hugo-toggle-draft)) + (buffer-string)))) + (should (string= second test-hugo-draft-true-post)))) + +;;; Boundary Cases + +(ert-deftest test-hugo-config-toggle-draft-boundary-extra-spaces () + "Should handle extra spaces between keyword and value." + (let ((result (test-hugo-toggle-draft-in-buffer "#+hugo_draft: true\n"))) + (should (string-match-p "#\\+hugo_draft: false" result)))) + +(ert-deftest test-hugo-config-toggle-draft-boundary-keyword-first-line () + "Should work when #+hugo_draft is the first line." + (let ((result (test-hugo-toggle-draft-in-buffer "#+hugo_draft: true\nContent."))) + (should (string-match-p "#\\+hugo_draft: false" result)) + (should (string-match-p "Content\\." result)))) + +(ert-deftest test-hugo-config-toggle-draft-boundary-keyword-last-line () + "Should work when #+hugo_draft is the last line." + (let ((result (test-hugo-toggle-draft-in-buffer "#+title: Test\n#+hugo_draft: false"))) + (should (string-match-p "#\\+hugo_draft: true" result)))) + +(ert-deftest test-hugo-config-toggle-draft-boundary-only-draft-line () + "Should work when buffer contains only the draft keyword." + (let ((result (test-hugo-toggle-draft-in-buffer "#+hugo_draft: true"))) + (should (string= result "#+hugo_draft: false")))) + +(ert-deftest test-hugo-config-toggle-draft-boundary-preserves-point () + "Point should be restored to original position after toggle." + (with-temp-buffer + (insert test-hugo-draft-true-post) + (goto-char 50) + (let ((original-point (point))) + (cl-letf (((symbol-function 'save-buffer) #'ignore)) + (cj/hugo-toggle-draft)) + (should (= (point) original-point))))) + +;;; Error Cases + +(ert-deftest test-hugo-config-toggle-draft-error-no-keyword () + "Should signal user-error when no #+hugo_draft keyword is present." + (should-error + (with-temp-buffer + (insert "#+title: A Post Without Draft\n#+hugo_tags: test\n") + (cl-letf (((symbol-function 'save-buffer) #'ignore)) + (cj/hugo-toggle-draft))) + :type 'user-error)) + +(ert-deftest test-hugo-config-toggle-draft-error-empty-buffer () + "Should signal user-error on empty buffer." + (should-error + (with-temp-buffer + (cl-letf (((symbol-function 'save-buffer) #'ignore)) + (cj/hugo-toggle-draft))) + :type 'user-error)) + +(ert-deftest test-hugo-config-toggle-draft-error-invalid-value () + "Should signal user-error when draft has unexpected value." + (should-error + (with-temp-buffer + (insert "#+hugo_draft: maybe\n") + (cl-letf (((symbol-function 'save-buffer) #'ignore)) + (cj/hugo-toggle-draft))) + :type 'user-error)) + +(provide 'test-hugo-config-toggle-draft) +;;; test-hugo-config-toggle-draft.el ends here diff --git a/tests/test-modeline-config-string-cut-middle.el b/tests/test-modeline-config-string-cut-middle.el new file mode 100644 index 00000000..40cc0bcc --- /dev/null +++ b/tests/test-modeline-config-string-cut-middle.el @@ -0,0 +1,145 @@ +;;; test-modeline-config-string-cut-middle.el --- Tests for cj/modeline-string-cut-middle -*- lexical-binding: t; -*- + +;;; Commentary: +;; Tests for the cj/modeline-string-cut-middle function from modeline-config.el +;; +;; This function truncates a string in the middle with "..." when conditions +;; are met (narrow window, multi-window, string exceeds truncation length). +;; Example: "my-very-long-name.el" → "my-ver...me.el" +;; +;; We mock cj/modeline-string-truncate-p to isolate the string manipulation +;; logic from window state checks. + +;;; Code: + +(require 'ert) + +;; Add modules directory to load path +(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory)) + +;; Stub dependencies before loading the module +(unless (boundp 'cj/buffer-status-colors) + (defvar cj/buffer-status-colors + '((unmodified . "#FFFFFF") + (modified . "#00FF00") + (read-only . "#FF0000") + (overwrite . "#FFD700")))) + +(require 'modeline-config) + +;;; Test Helpers + +(defmacro with-truncation-enabled (&rest body) + "Execute BODY with truncation forced on. +Mocks `cj/modeline-string-truncate-p' to always return t." + `(cl-letf (((symbol-function 'cj/modeline-string-truncate-p) + (lambda (_str) t))) + ,@body)) + +(defmacro with-truncation-disabled (&rest body) + "Execute BODY with truncation forced off. +Mocks `cj/modeline-string-truncate-p' to always return nil." + `(cl-letf (((symbol-function 'cj/modeline-string-truncate-p) + (lambda (_str) nil))) + ,@body)) + +;;; Normal Cases + +(ert-deftest test-modeline-config-string-cut-middle-normal-truncates-long-string () + "Should truncate a long string in the middle with '...'." + (with-truncation-enabled + (let* ((cj/modeline-string-truncate-length 12) + (result (cj/modeline-string-cut-middle "my-very-long-name.el"))) + (should (string-match-p "\\.\\.\\." result)) + (should (< (length result) (length "my-very-long-name.el")))))) + +(ert-deftest test-modeline-config-string-cut-middle-normal-preserves-start-and-end () + "Should keep the beginning and end of the string." + (with-truncation-enabled + (let* ((cj/modeline-string-truncate-length 12) + (result (cj/modeline-string-cut-middle "my-very-long-name.el"))) + (should (string-prefix-p "my-ver" result)) + (should (string-suffix-p "me.el" result))))) + +(ert-deftest test-modeline-config-string-cut-middle-normal-result-length () + "Truncated result should be truncate-length + 3 (for '...')." + (with-truncation-enabled + (let* ((cj/modeline-string-truncate-length 12) + (result (cj/modeline-string-cut-middle "abcdefghijklmnopqrstuvwxyz"))) + ;; half=6, so 6 chars + "..." + 6 chars = 15 + (should (= (length result) 15))))) + +(ert-deftest test-modeline-config-string-cut-middle-normal-no-truncation-when-disabled () + "Should return string unchanged when truncation is disabled." + (with-truncation-disabled + (let ((result (cj/modeline-string-cut-middle "my-very-long-name.el"))) + (should (string= result "my-very-long-name.el"))))) + +(ert-deftest test-modeline-config-string-cut-middle-normal-short-string-unchanged () + "Short string should pass through unchanged when truncation is disabled." + (with-truncation-disabled + (let ((result (cj/modeline-string-cut-middle "init.el"))) + (should (string= result "init.el"))))) + +;;; Boundary Cases + +(ert-deftest test-modeline-config-string-cut-middle-boundary-empty-string () + "Empty string should pass through unchanged." + (with-truncation-disabled + (let ((result (cj/modeline-string-cut-middle ""))) + (should (string= result ""))))) + +(ert-deftest test-modeline-config-string-cut-middle-boundary-single-char () + "Single character string should pass through unchanged." + (with-truncation-disabled + (let ((result (cj/modeline-string-cut-middle "x"))) + (should (string= result "x"))))) + +(ert-deftest test-modeline-config-string-cut-middle-boundary-odd-truncate-length () + "Odd truncation length should floor the half correctly." + (with-truncation-enabled + (let* ((cj/modeline-string-truncate-length 11) + (result (cj/modeline-string-cut-middle "abcdefghijklmnopqrstuvwxyz"))) + ;; half = (floor 11 2) = 5, so 5 + "..." + 5 = 13 + (should (= (length result) 13)) + (should (string= (substring result 0 5) "abcde")) + (should (string= (substring result -5) "vwxyz"))))) + +(ert-deftest test-modeline-config-string-cut-middle-boundary-truncate-length-2 () + "Minimum practical truncation length of 2." + (with-truncation-enabled + (let* ((cj/modeline-string-truncate-length 2) + (result (cj/modeline-string-cut-middle "abcdefghij"))) + ;; half = (floor 2 2) = 1, so 1 + "..." + 1 = 5 + (should (= (length result) 5)) + (should (string= result "a...j"))))) + +(ert-deftest test-modeline-config-string-cut-middle-boundary-unicode-filename () + "Should handle unicode characters in filename." + (with-truncation-enabled + (let* ((cj/modeline-string-truncate-length 10) + (result (cj/modeline-string-cut-middle "überlangerDateiname.el"))) + (should (string-match-p "\\.\\.\\." result)) + (should (< (length result) (length "überlangerDateiname.el")))))) + +(ert-deftest test-modeline-config-string-cut-middle-boundary-dots-in-filename () + "Should handle filenames with dots (common in elisp)." + (with-truncation-enabled + (let* ((cj/modeline-string-truncate-length 12) + (result (cj/modeline-string-cut-middle "test-custom-whitespace-collapse.el"))) + (should (string-match-p "\\.\\.\\." result))))) + +;;; Error Cases + +(ert-deftest test-modeline-config-string-cut-middle-error-nil-input () + "Nil input should pass through (truncate-p returns nil for non-strings)." + (with-truncation-disabled + (should (null (cj/modeline-string-cut-middle nil))))) + +(ert-deftest test-modeline-config-string-cut-middle-error-number-input () + "Number input should pass through (truncate-p returns nil for non-strings)." + (with-truncation-disabled + (should (= (cj/modeline-string-cut-middle 42) 42)))) + +(provide 'test-modeline-config-string-cut-middle) +;;; test-modeline-config-string-cut-middle.el ends here diff --git a/tests/test-modeline-config-string-truncate-p.el b/tests/test-modeline-config-string-truncate-p.el new file mode 100644 index 00000000..09378b0d --- /dev/null +++ b/tests/test-modeline-config-string-truncate-p.el @@ -0,0 +1,132 @@ +;;; test-modeline-config-string-truncate-p.el --- Tests for cj/modeline-string-truncate-p -*- lexical-binding: t; -*- + +;;; Commentary: +;; Tests for the cj/modeline-string-truncate-p function from modeline-config.el +;; +;; This function returns non-nil when ALL conditions are met: +;; 1. STR is a string +;; 2. STR is not empty +;; 3. Window is narrow (< 100 chars via cj/modeline-window-narrow-p) +;; 4. String length exceeds cj/modeline-string-truncate-length +;; 5. Not in a single-window frame (one-window-p returns nil) +;; +;; We mock window functions to isolate the logic. + +;;; Code: + +(require 'ert) + +;; Add modules directory to load path +(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory)) + +;; Stub dependencies before loading the module +(unless (boundp 'cj/buffer-status-colors) + (defvar cj/buffer-status-colors + '((unmodified . "#FFFFFF") + (modified . "#00FF00") + (read-only . "#FF0000") + (overwrite . "#FFD700")))) + +(require 'modeline-config) + +;;; Test Helpers + +(defmacro with-window-state (narrow-p one-window-p &rest body) + "Execute BODY with mocked window state. +NARROW-P controls `cj/modeline-window-narrow-p' return value. +ONE-WINDOW-P controls `one-window-p' return value." + (declare (indent 2)) + `(cl-letf (((symbol-function 'cj/modeline-window-narrow-p) + (lambda () ,narrow-p)) + ((symbol-function 'one-window-p) + (lambda (&rest _args) ,one-window-p))) + ,@body)) + +;;; Normal Cases - All conditions met (should return non-nil) + +(ert-deftest test-modeline-config-string-truncate-p-normal-long-string-narrow-multi-window () + "Should return non-nil when string is long, window is narrow, and multiple windows." + (with-window-state t nil + (let ((cj/modeline-string-truncate-length 12)) + (should (cj/modeline-string-truncate-p "a-very-long-filename.el"))))) + +(ert-deftest test-modeline-config-string-truncate-p-normal-exactly-over-length () + "Should return non-nil when string is one char over truncation length." + (with-window-state t nil + (let ((cj/modeline-string-truncate-length 12)) + (should (cj/modeline-string-truncate-p "1234567890123"))))) ; 13 chars + +;;; Normal Cases - Conditions not met (should return nil) + +(ert-deftest test-modeline-config-string-truncate-p-normal-short-string () + "Should return nil when string is shorter than truncation length." + (with-window-state t nil + (let ((cj/modeline-string-truncate-length 12)) + (should-not (cj/modeline-string-truncate-p "init.el"))))) + +(ert-deftest test-modeline-config-string-truncate-p-normal-wide-window () + "Should return nil when window is wide (not narrow)." + (with-window-state nil nil + (let ((cj/modeline-string-truncate-length 12)) + (should-not (cj/modeline-string-truncate-p "a-very-long-filename.el"))))) + +(ert-deftest test-modeline-config-string-truncate-p-normal-single-window () + "Should return nil when only one window is open." + (with-window-state t t + (let ((cj/modeline-string-truncate-length 12)) + (should-not (cj/modeline-string-truncate-p "a-very-long-filename.el"))))) + +;;; Boundary Cases + +(ert-deftest test-modeline-config-string-truncate-p-boundary-exact-length () + "Should return nil when string is exactly at truncation length." + (with-window-state t nil + (let ((cj/modeline-string-truncate-length 12)) + (should-not (cj/modeline-string-truncate-p "123456789012"))))) ; exactly 12 + +(ert-deftest test-modeline-config-string-truncate-p-boundary-empty-string () + "Should return nil for empty string." + (with-window-state t nil + (let ((cj/modeline-string-truncate-length 12)) + (should-not (cj/modeline-string-truncate-p ""))))) + +(ert-deftest test-modeline-config-string-truncate-p-boundary-single-char () + "Should return nil for single character string." + (with-window-state t nil + (let ((cj/modeline-string-truncate-length 12)) + (should-not (cj/modeline-string-truncate-p "x"))))) + +(ert-deftest test-modeline-config-string-truncate-p-boundary-truncate-length-1 () + "Should return non-nil for any string > 1 when truncation length is 1." + (with-window-state t nil + (let ((cj/modeline-string-truncate-length 1)) + (should (cj/modeline-string-truncate-p "ab"))))) + +(ert-deftest test-modeline-config-string-truncate-p-boundary-truncate-length-0 () + "Should return non-nil for any non-empty string when truncation length is 0." + (with-window-state t nil + (let ((cj/modeline-string-truncate-length 0)) + (should (cj/modeline-string-truncate-p "a"))))) + +;;; Error Cases + +(ert-deftest test-modeline-config-string-truncate-p-error-nil-input () + "Should return nil for nil input." + (with-window-state t nil + (let ((cj/modeline-string-truncate-length 12)) + (should-not (cj/modeline-string-truncate-p nil))))) + +(ert-deftest test-modeline-config-string-truncate-p-error-number-input () + "Should return nil for number input." + (with-window-state t nil + (let ((cj/modeline-string-truncate-length 12)) + (should-not (cj/modeline-string-truncate-p 42))))) + +(ert-deftest test-modeline-config-string-truncate-p-error-symbol-input () + "Should return nil for symbol input." + (with-window-state t nil + (let ((cj/modeline-string-truncate-length 12)) + (should-not (cj/modeline-string-truncate-p 'some-symbol))))) + +(provide 'test-modeline-config-string-truncate-p) +;;; test-modeline-config-string-truncate-p.el ends here |
