diff options
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 | ||||
| -rw-r--r-- | tests/test-org-reveal-config-header-template.el | 144 | ||||
| -rw-r--r-- | tests/test-org-reveal-config-title-to-filename.el | 109 |
8 files changed, 1015 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 diff --git a/tests/test-org-reveal-config-header-template.el b/tests/test-org-reveal-config-header-template.el new file mode 100644 index 00000000..11939c55 --- /dev/null +++ b/tests/test-org-reveal-config-header-template.el @@ -0,0 +1,144 @@ +;;; test-org-reveal-config-header-template.el --- Tests for cj/--reveal-header-template -*- lexical-binding: t; -*- + +;;; Commentary: +;; Tests for the cj/--reveal-header-template function from org-reveal-config.el +;; +;; This function takes a title string and returns a complete #+REVEAL_ header +;; block for an org-reveal presentation. It uses user-full-name and +;; format-time-string internally, so we mock those for deterministic output. +;; The reveal.js constants (root, theme, transition) are tested via their +;; presence in the output. + +;;; Code: + +(require 'ert) + +;; Add modules directory to load path +(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory)) + +;; Stub ox-reveal dependency (not available in batch mode) +(provide 'ox-reveal) + +(require 'org-reveal-config) + +;; Helper to call template with deterministic date and author +(defun test-reveal--header (title) + "Call cj/--reveal-header-template with TITLE, mocking time and user." + (cl-letf (((symbol-function 'user-full-name) (lambda () "Test Author")) + ((symbol-function 'format-time-string) + (lambda (_fmt) "2026-02-14"))) + (cj/--reveal-header-template title))) + +;;; Normal Cases + +(ert-deftest test-org-reveal-config-header-template-normal-contains-title () + "Output should contain #+TITLE: with the given title." + (let ((result (test-reveal--header "My Talk"))) + (should (string-match-p "^#\\+TITLE: My Talk$" result)))) + +(ert-deftest test-org-reveal-config-header-template-normal-contains-author () + "Output should contain #+AUTHOR: with the user's full name." + (let ((result (test-reveal--header "My Talk"))) + (should (string-match-p "^#\\+AUTHOR: Test Author$" result)))) + +(ert-deftest test-org-reveal-config-header-template-normal-contains-date () + "Output should contain #+DATE: with today's date." + (let ((result (test-reveal--header "My Talk"))) + (should (string-match-p "^#\\+DATE: 2026-02-14$" result)))) + +(ert-deftest test-org-reveal-config-header-template-normal-contains-reveal-root () + "Output should contain #+REVEAL_ROOT: with file:// URL." + (let ((result (test-reveal--header "My Talk"))) + (should (string-match-p "^#\\+REVEAL_ROOT: file://" result)))) + +(ert-deftest test-org-reveal-config-header-template-normal-contains-theme () + "Output should contain #+REVEAL_THEME: with the default theme." + (let ((result (test-reveal--header "My Talk"))) + (should (string-match-p + (format "^#\\+REVEAL_THEME: %s$" cj/reveal-default-theme) + result)))) + +(ert-deftest test-org-reveal-config-header-template-normal-contains-transition () + "Output should contain #+REVEAL_TRANS: with the default transition." + (let ((result (test-reveal--header "My Talk"))) + (should (string-match-p + (format "^#\\+REVEAL_TRANS: %s$" cj/reveal-default-transition) + result)))) + +(ert-deftest test-org-reveal-config-header-template-normal-contains-init-options () + "Output should contain #+REVEAL_INIT_OPTIONS: with slideNumber and hash." + (let ((result (test-reveal--header "My Talk"))) + (should (string-match-p "^#\\+REVEAL_INIT_OPTIONS:.*slideNumber" result)) + (should (string-match-p "^#\\+REVEAL_INIT_OPTIONS:.*hash" result)))) + +(ert-deftest test-org-reveal-config-header-template-normal-contains-plugins () + "Output should contain #+REVEAL_PLUGINS: listing all plugins." + (let ((result (test-reveal--header "My Talk"))) + (should (string-match-p "^#\\+REVEAL_PLUGINS:.*highlight" result)) + (should (string-match-p "^#\\+REVEAL_PLUGINS:.*notes" result)) + (should (string-match-p "^#\\+REVEAL_PLUGINS:.*search" result)) + (should (string-match-p "^#\\+REVEAL_PLUGINS:.*zoom" result)))) + +(ert-deftest test-org-reveal-config-header-template-normal-contains-highlight-css () + "Output should contain #+REVEAL_HIGHLIGHT_CSS: with monokai." + (let ((result (test-reveal--header "My Talk"))) + (should (string-match-p "^#\\+REVEAL_HIGHLIGHT_CSS:.*monokai" result)))) + +(ert-deftest test-org-reveal-config-header-template-normal-contains-options () + "Output should contain #+OPTIONS: disabling toc and num." + (let ((result (test-reveal--header "My Talk"))) + (should (string-match-p "^#\\+OPTIONS: toc:nil num:nil$" result)))) + +(ert-deftest test-org-reveal-config-header-template-normal-ends-with-blank-line () + "Output should end with a trailing newline (blank line separator)." + (let ((result (test-reveal--header "My Talk"))) + (should (string-suffix-p "\n\n" result)))) + +(ert-deftest test-org-reveal-config-header-template-normal-all-keywords-present () + "All required org keywords should be present in the output." + (let ((result (test-reveal--header "My Talk")) + (keywords '("#+TITLE:" "#+AUTHOR:" "#+DATE:" + "#+REVEAL_ROOT:" "#+REVEAL_THEME:" "#+REVEAL_TRANS:" + "#+REVEAL_INIT_OPTIONS:" "#+REVEAL_PLUGINS:" + "#+REVEAL_HIGHLIGHT_CSS:" "#+OPTIONS:"))) + (dolist (kw keywords) + (should (string-match-p (regexp-quote kw) result))))) + +;;; Boundary Cases + +(ert-deftest test-org-reveal-config-header-template-boundary-empty-title () + "Empty title should produce valid header with empty #+TITLE." + (let ((result (test-reveal--header ""))) + (should (string-match-p "^#\\+TITLE: $" result)) + (should (string-match-p "#\\+REVEAL_THEME:" result)))) + +(ert-deftest test-org-reveal-config-header-template-boundary-unicode-title () + "Unicode title should be preserved in #+TITLE." + (let ((result (test-reveal--header "日本語プレゼン"))) + (should (string-match-p "^#\\+TITLE: 日本語プレゼン$" result)))) + +(ert-deftest test-org-reveal-config-header-template-boundary-title-with-special-chars () + "Special characters in title should not break the template." + (let ((result (test-reveal--header "What's New? (2026 Edition)"))) + (should (string-match-p "^#\\+TITLE: What's New\\? (2026 Edition)$" result)))) + +(ert-deftest test-org-reveal-config-header-template-boundary-title-with-percent () + "Percent signs in title should not break format string." + (let ((result (test-reveal--header "100% Complete"))) + (should (string-match-p "^#\\+TITLE: 100% Complete$" result)))) + +(ert-deftest test-org-reveal-config-header-template-boundary-very-long-title () + "Very long title should produce valid output." + (let* ((long-title (make-string 200 ?x)) + (result (test-reveal--header long-title))) + (should (string-match-p "#\\+TITLE:" result)) + (should (string-match-p "#\\+REVEAL_THEME:" result)))) + +;;; Error Cases + +(ert-deftest test-org-reveal-config-header-template-error-nil-title () + "Nil title should signal an error rather than silently producing garbage." + (should-error (test-reveal--header nil) :type 'user-error)) + +(provide 'test-org-reveal-config-header-template) +;;; test-org-reveal-config-header-template.el ends here diff --git a/tests/test-org-reveal-config-title-to-filename.el b/tests/test-org-reveal-config-title-to-filename.el new file mode 100644 index 00000000..46296e68 --- /dev/null +++ b/tests/test-org-reveal-config-title-to-filename.el @@ -0,0 +1,109 @@ +;;; test-org-reveal-config-title-to-filename.el --- Tests for cj/--reveal-title-to-filename -*- lexical-binding: t; -*- + +;;; Commentary: +;; Tests for the cj/--reveal-title-to-filename function from org-reveal-config.el +;; +;; This function takes a presentation title string, downcases it, replaces +;; whitespace runs with hyphens, and appends ".org". It is a pure string +;; function with no external dependencies — zero mocking required. + +;;; Code: + +(require 'ert) + +;; Add modules directory to load path +(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory)) + +;; Stub ox-reveal dependency (not available in batch mode) +(provide 'ox-reveal) + +(require 'org-reveal-config) + +;;; Normal Cases + +(ert-deftest test-org-reveal-config-title-to-filename-normal-simple-title () + "Simple title should become lowercase-hyphenated.org." + (should (equal "my-first-talk.org" + (cj/--reveal-title-to-filename "My First Talk")))) + +(ert-deftest test-org-reveal-config-title-to-filename-normal-two-words () + "Two-word title should produce single hyphen." + (should (equal "hello-world.org" + (cj/--reveal-title-to-filename "Hello World")))) + +(ert-deftest test-org-reveal-config-title-to-filename-normal-already-lowercase () + "Already lowercase title should be unchanged except for extension." + (should (equal "demo-talk.org" + (cj/--reveal-title-to-filename "demo talk")))) + +(ert-deftest test-org-reveal-config-title-to-filename-normal-single-word () + "Single word title should just get .org appended." + (should (equal "overview.org" + (cj/--reveal-title-to-filename "Overview")))) + +(ert-deftest test-org-reveal-config-title-to-filename-normal-always-org-extension () + "Result should always end with .org." + (should (string-suffix-p ".org" + (cj/--reveal-title-to-filename "Anything")))) + +;;; Boundary Cases + +(ert-deftest test-org-reveal-config-title-to-filename-boundary-multiple-spaces () + "Multiple consecutive spaces should collapse to single hyphen." + (should (equal "foo-bar.org" + (cj/--reveal-title-to-filename "foo bar")))) + +(ert-deftest test-org-reveal-config-title-to-filename-boundary-tabs () + "Tabs should be treated as whitespace and replaced." + (should (equal "tab-separated.org" + (cj/--reveal-title-to-filename "tab\tseparated")))) + +(ert-deftest test-org-reveal-config-title-to-filename-boundary-mixed-whitespace () + "Mixed spaces and tabs should collapse to single hyphen." + (should (equal "mixed-ws.org" + (cj/--reveal-title-to-filename "mixed \t ws")))) + +(ert-deftest test-org-reveal-config-title-to-filename-boundary-leading-trailing-spaces () + "Leading and trailing spaces become leading/trailing hyphens." + (let ((result (cj/--reveal-title-to-filename " padded "))) + (should (equal "-padded-.org" result)))) + +(ert-deftest test-org-reveal-config-title-to-filename-boundary-unicode-title () + "Unicode characters should be preserved (only whitespace replaced)." + (should (equal "日本語-talk.org" + (cj/--reveal-title-to-filename "日本語 Talk")))) + +(ert-deftest test-org-reveal-config-title-to-filename-boundary-numbers-in-title () + "Numbers should be preserved in slug." + (should (equal "q4-2026-results.org" + (cj/--reveal-title-to-filename "Q4 2026 Results")))) + +(ert-deftest test-org-reveal-config-title-to-filename-boundary-special-chars-preserved () + "Non-whitespace special characters should be preserved (not stripped)." + (should (equal "what's-new?.org" + (cj/--reveal-title-to-filename "What's New?")))) + +(ert-deftest test-org-reveal-config-title-to-filename-boundary-very-long-title () + "Very long title should still produce valid filename." + (let* ((long-title (mapconcat #'identity (make-list 20 "word") " ")) + (result (cj/--reveal-title-to-filename long-title))) + (should (string-suffix-p ".org" result)) + (should-not (string-match-p " " result)))) + +(ert-deftest test-org-reveal-config-title-to-filename-boundary-empty-string () + "Empty string should produce just .org." + (should (equal ".org" (cj/--reveal-title-to-filename "")))) + +(ert-deftest test-org-reveal-config-title-to-filename-boundary-newline () + "Newlines should be treated as whitespace." + (should (equal "line-one-line-two.org" + (cj/--reveal-title-to-filename "Line One\nLine Two")))) + +;;; Error Cases + +(ert-deftest test-org-reveal-config-title-to-filename-error-nil-input () + "Nil input should signal an error (not crash silently)." + (should-error (cj/--reveal-title-to-filename nil))) + +(provide 'test-org-reveal-config-title-to-filename) +;;; test-org-reveal-config-title-to-filename.el ends here |
