diff options
Diffstat (limited to 'tests')
| -rw-r--r-- | tests/test-custom-buffer-file-keymap-bindings.el | 30 | ||||
| -rw-r--r-- | tests/test-dashboard-config-recentf-exclude.el | 33 | ||||
| -rw-r--r-- | tests/test-dwim-shell-config-command-fixes.el | 33 | ||||
| -rw-r--r-- | tests/test-help-config.el | 32 | ||||
| -rw-r--r-- | tests/test-mail-config-refile-folder.el | 40 | ||||
| -rw-r--r-- | tests/test-markdown-config.el | 10 | ||||
| -rw-r--r-- | tests/test-org-capture-config-popup-window.el | 281 | ||||
| -rw-r--r-- | tests/test-org-drill-config-commands.el | 45 | ||||
| -rw-r--r-- | tests/test-org-roam-config-dailies-head.el | 29 | ||||
| -rw-r--r-- | tests/test-prog-general--electric-pair-angle.el | 54 | ||||
| -rw-r--r-- | tests/test-reconcile--find-git-repos.el | 9 | ||||
| -rw-r--r-- | tests/test-selection-framework--consult-line-or-repeat.el | 6 | ||||
| -rw-r--r-- | tests/test-system-lib-confirm-strong.el | 37 | ||||
| -rw-r--r-- | tests/test-ui-navigation-split-follow-undo-kill.el | 29 |
14 files changed, 651 insertions, 17 deletions
diff --git a/tests/test-custom-buffer-file-keymap-bindings.el b/tests/test-custom-buffer-file-keymap-bindings.el new file mode 100644 index 00000000..ea9ceb26 --- /dev/null +++ b/tests/test-custom-buffer-file-keymap-bindings.el @@ -0,0 +1,30 @@ +;;; test-custom-buffer-file-keymap-bindings.el --- d/D bindings in the buffer-and-file keymap -*- lexical-binding: t; -*- + +;;; Commentary: +;; `cj/buffer-and-file-map' should put the destructive op on the capital key and +;; the frequently-used op on the easy lowercase key: D = delete-buffer-and-file, +;; d = diff-buffer-with-file. Guards the swap against silently reverting. + +;;; Code: + +(require 'ert) + +(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory)) + +;; Stub dependencies before loading the module (mirrors the sibling tests). +(defvar cj/custom-keymap (make-sparse-keymap) + "Stub keymap for testing.") +(provide 'ps-print) + +(require 'custom-buffer-file) + +(ert-deftest test-custom-buffer-file-keymap-diff-on-lowercase-d () + "Normal: lowercase d runs diff -- the frequently-used, non-destructive op." + (should (eq (keymap-lookup cj/buffer-and-file-map "d") #'cj/diff-buffer-with-file))) + +(ert-deftest test-custom-buffer-file-keymap-delete-on-capital-d () + "Normal: capital D runs delete -- the destructive op on the capital key." + (should (eq (keymap-lookup cj/buffer-and-file-map "D") #'cj/delete-buffer-and-file))) + +(provide 'test-custom-buffer-file-keymap-bindings) +;;; test-custom-buffer-file-keymap-bindings.el ends here diff --git a/tests/test-dashboard-config-recentf-exclude.el b/tests/test-dashboard-config-recentf-exclude.el new file mode 100644 index 00000000..f35b3eda --- /dev/null +++ b/tests/test-dashboard-config-recentf-exclude.el @@ -0,0 +1,33 @@ +;;; test-dashboard-config-recentf-exclude.el --- recentf-exclude is not clobbered -*- lexical-binding: t; -*- + +;;; Commentary: +;; `cj/--dashboard-exclude-emms-from-recentf' adds the EMMS history pattern +;; to `recentf-exclude'. It must ADD to the list, not replace it, or it +;; wipes the exclusions system-defaults.el set earlier in init order +;; (emacs_bookmarks, elpa, recentf, ElfeedDB, airootfs). + +;;; Code: + +(require 'ert) +(require 'recentf) ; makes `recentf-exclude' special so the let below is dynamic + +(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory)) +(require 'testutil-general) +(require 'dashboard-config) + +(ert-deftest test-dashboard-config-exclude-emms-preserves-existing-entries () + "Error: excluding the EMMS history preserves prior recentf-exclude entries." + (let ((recentf-exclude (list "emacs_bookmarks" "airootfs"))) + (cj/--dashboard-exclude-emms-from-recentf) + (should (member "/emms/history" recentf-exclude)) + (should (member "emacs_bookmarks" recentf-exclude)) + (should (member "airootfs" recentf-exclude)))) + +(ert-deftest test-dashboard-config-exclude-emms-adds-the-pattern () + "Normal: the EMMS history pattern is present after the call." + (let ((recentf-exclude nil)) + (cj/--dashboard-exclude-emms-from-recentf) + (should (member "/emms/history" recentf-exclude)))) + +(provide 'test-dashboard-config-recentf-exclude) +;;; test-dashboard-config-recentf-exclude.el ends here diff --git a/tests/test-dwim-shell-config-command-fixes.el b/tests/test-dwim-shell-config-command-fixes.el new file mode 100644 index 00000000..2f49a868 --- /dev/null +++ b/tests/test-dwim-shell-config-command-fixes.el @@ -0,0 +1,33 @@ +;;; test-dwim-shell-config-command-fixes.el --- zip/backup command builders -*- lexical-binding: t; -*- + +;;; Commentary: +;; Two audit fixes, extracted into top-level command-string builders so they're +;; testable without loading the dwim-shell-command package (the command defuns +;; that call them live inside its use-package :config, which the batch test +;; harness doesn't instantiate): +;; - cj/dwim-shell--zip-single-file-command names the archive <fne>.zip +;; - cj/dwim-shell--dated-backup-command carries a real timestamp, not "$(date)" +;; The third fix (dired menu key M-S-d -> M-D) is a keybinding inside the same +;; :config block; it's verified in the live daemon, not here. + +;;; Code: + +(require 'ert) +(require 'dwim-shell-config) + +(ert-deftest test-dwim-zip-single-file-command-names-archive-dot-zip () + "Normal: the single-file zip template names the archive <fne>.zip, with no +leftover <<e>> that would rebuild the input filename." + (let ((cmd (cj/dwim-shell--zip-single-file-command))) + (should (string-match-p "'<<fne>>\\.zip'" cmd)) + (should-not (string-match-p "<<e>>" cmd)))) + +(ert-deftest test-dwim-dated-backup-command-carries-real-timestamp () + "Normal: the dated-backup template interpolates a real YYYYMMDD_HHMMSS stamp, +so the substitution can't sit dead inside single quotes." + (let ((cmd (cj/dwim-shell--dated-backup-command))) + (should (string-match-p "\\.[0-9]\\{8\\}_[0-9]\\{6\\}\\.bak'" cmd)) + (should-not (string-match-p "\\$(date" cmd)))) + +(provide 'test-dwim-shell-config-command-fixes) +;;; test-dwim-shell-config-command-fixes.el ends here diff --git a/tests/test-help-config.el b/tests/test-help-config.el new file mode 100644 index 00000000..0ba95c41 --- /dev/null +++ b/tests/test-help-config.el @@ -0,0 +1,32 @@ +;;; test-help-config.el --- Tests for the Info-open decision logic -*- lexical-binding: t; -*- + +;;; Commentary: +;; cj/open-with-info-mode opens the current .info buffer in Info, prompting to +;; save first if the buffer is modified. The save/cancel/open decision is +;; factored into the pure helper `cj/--info-open-plan' so it's testable without +;; driving find-file, Info, or the save prompt. Declining the prompt must yield +;; `cancel' -- the original cl-return-from inside a plain defun signalled +;; "No catch for tag" instead of cancelling. + +;;; Code: + +(require 'ert) + +(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory)) +(require 'help-config) + +(ert-deftest test-info-open-plan-unmodified-opens () + "Normal: an unmodified buffer opens in Info directly." + (should (eq (cj/--info-open-plan nil nil) 'open))) + +(ert-deftest test-info-open-plan-modified-confirmed-saves-then-opens () + "Normal: a modified buffer whose save is confirmed saves, then opens." + (should (eq (cj/--info-open-plan t t) 'save-then-open))) + +(ert-deftest test-info-open-plan-modified-declined-cancels () + "Error/edge: a modified buffer whose save is declined cancels -- the path that +used to signal \"No catch for tag\" via cl-return-from in a plain defun." + (should (eq (cj/--info-open-plan t nil) 'cancel))) + +(provide 'test-help-config) +;;; test-help-config.el ends here diff --git a/tests/test-mail-config-refile-folder.el b/tests/test-mail-config-refile-folder.el new file mode 100644 index 00000000..e2d224eb --- /dev/null +++ b/tests/test-mail-config-refile-folder.el @@ -0,0 +1,40 @@ +;;; test-mail-config-refile-folder.el --- Tests for refile-folder dispatch -*- lexical-binding: t; -*- + +;;; Commentary: +;; ERT tests for `cj/mu4e--refile-folder-for-maildir', the per-message refile +;; (archive) target dispatch. cmail has a real synced Archive folder; the +;; Gmail-backed accounts (gmail, dmail) have none, so refiling them must signal +;; rather than move mail into an unsynced, phantom folder (silent mail loss). + +;;; Code: + +(require 'ert) + +(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory)) +(require 'mail-config) + +(ert-deftest test-mail-config-refile-cmail-returns-archive () + "Normal: a cmail message refiles into the synced /cmail/Archive folder." + (should (string= (cj/mu4e--refile-folder-for-maildir "/cmail/INBOX") + "/cmail/Archive")) + (should (string= (cj/mu4e--refile-folder-for-maildir "/cmail/Sent") + "/cmail/Archive"))) + +(ert-deftest test-mail-config-refile-gmail-signals () + "Error: gmail has no synced archive folder, so refile signals rather than +moving mail into a phantom folder." + (should-error (cj/mu4e--refile-folder-for-maildir "/gmail/INBOX") + :type 'user-error)) + +(ert-deftest test-mail-config-refile-dmail-signals () + "Error: dmail (Gmail-backed) has no synced archive folder; refile signals." + (should-error (cj/mu4e--refile-folder-for-maildir "/dmail/INBOX") + :type 'user-error)) + +(ert-deftest test-mail-config-refile-nil-maildir-signals () + "Boundary: a message with no maildir cannot be refiled; signal." + (should-error (cj/mu4e--refile-folder-for-maildir nil) + :type 'user-error)) + +(provide 'test-mail-config-refile-folder) +;;; test-mail-config-refile-folder.el ends here diff --git a/tests/test-markdown-config.el b/tests/test-markdown-config.el index 45e1a601..edb20d35 100644 --- a/tests/test-markdown-config.el +++ b/tests/test-markdown-config.el @@ -9,6 +9,7 @@ ;;; Code: (require 'ert) +(require 'cl-lib) (add-to-list 'load-path (expand-file-name "modules" user-emacs-directory)) @@ -50,5 +51,14 @@ (should (string-match-p "<xmp" (buffer-string)))) (kill-buffer src)))) +;;; cj/markdown-preview (guard: refuse when the httpd listener is down) + +(ert-deftest test-markdown-preview-errors-when-server-down () + "Error: `cj/markdown-preview' signals a user-error when the simple-httpd +listener is not running, rather than opening a preview against a dead server. +Also pins the rename off the bare `markdown-preview' that markdown-mode shadows." + (cl-letf (((symbol-function 'httpd-running-p) (lambda () nil))) + (should-error (cj/markdown-preview) :type 'user-error))) + (provide 'test-markdown-config) ;;; test-markdown-config.el ends here diff --git a/tests/test-org-capture-config-popup-window.el b/tests/test-org-capture-config-popup-window.el new file mode 100644 index 00000000..34f67b36 --- /dev/null +++ b/tests/test-org-capture-config-popup-window.el @@ -0,0 +1,281 @@ +;;; test-org-capture-config-popup-window.el --- Quick-capture popup single-window tests -*- lexical-binding: t; -*- + +;;; Commentary: +;; Tests for the pure predicate behind the quick-capture popup single-window +;; fix. The Hyprland Super+Shift+N popup opens an emacsclient frame named +;; "org-capture"; in that frame the *Org Select* template menu and the +;; CAPTURE-* buffer must fill the frame's sole window instead of splitting it. +;; `cj/org-capture--popup-sole-window-p' is the frame+buffer decision; the +;; display-buffer action that acts on it is exercised by hand (window ops), +;; not here. + +;;; Code: + +(require 'ert) +(require 'cl-lib) +(require 'org) +(require 'org-capture) ; makes `org-capture-templates' a real special var +(require 'user-constants) +(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory)) +(require 'org-capture-config) + +(defconst test-org-capture-popup--sample-templates + '(("t" "Task" entry (function cj/--org-capture-project-location) + "* TODO %?" :prepend t) + ("b" "Bug" entry (function cj/--org-capture-project-location) + "* TODO [#C] %?" :prepend t) + ("e" "Event" entry (file+headline schedule-file "Scheduled Events") + "* %?" :prepend t :prepare-finalize cj/org-capture-format-event-headline) + ("m" "Mu4e Email" entry (file+headline inbox-file "Inbox") "* TODO %?" :prepend t) + ("L" "Link" entry (file+headline inbox-file "Inbox") "* %?" :immediate-finish t) + ("d" "Drill Question" entry (file ignore) "* Item :drill:\n%?" :prepend t)) + "A representative org-capture-templates list for popup-subset tests.") + +;;; cj/org-capture--popup-sole-window-p + +(ert-deftest test-org-capture-config-popup-sole-window-p-select-menu () + "Normal: the *Org Select* menu in the popup frame wants the sole window." + (should (cj/org-capture--popup-sole-window-p "org-capture" "*Org Select*"))) + +(ert-deftest test-org-capture-config-popup-sole-window-p-capture-buffer () + "Normal: a CAPTURE-* buffer in the popup frame wants the sole window." + (should (cj/org-capture--popup-sole-window-p "org-capture" "CAPTURE-todo.org"))) + +(ert-deftest test-org-capture-config-popup-sole-window-p-capture-prefix-only () + "Boundary: the bare \"CAPTURE-\" prefix still matches." + (should (cj/org-capture--popup-sole-window-p "org-capture" "CAPTURE-"))) + +(ert-deftest test-org-capture-config-popup-sole-window-p-other-frame () + "Boundary: the same menu in a normal frame is left alone." + (should-not (cj/org-capture--popup-sole-window-p "emacs" "*Org Select*")) + (should-not (cj/org-capture--popup-sole-window-p nil "CAPTURE-todo.org"))) + +(ert-deftest test-org-capture-config-popup-sole-window-p-other-buffer () + "Boundary: an unrelated buffer in the popup frame is left alone." + (should-not (cj/org-capture--popup-sole-window-p "org-capture" "todo.org")) + (should-not (cj/org-capture--popup-sole-window-p "org-capture" "*scratch*"))) + +(ert-deftest test-org-capture-config-popup-sole-window-p-nil-buffer () + "Error: a nil or non-string buffer name returns nil without raising." + (should-not (cj/org-capture--popup-sole-window-p "org-capture" nil)) + (should-not (cj/org-capture--popup-sole-window-p "org-capture" 42))) + +;;; Integration: the display-buffer-alist entry routes to a sole window + +(ert-deftest test-integration-org-capture-popup-display-sole-window () + "Integration: in an \"org-capture\"-named frame, displaying a CAPTURE-* +buffer fills the frame's sole window via the registered display-buffer-alist +entry, instead of splitting. + +Components integrated: +- cj/org-capture--popup-display-condition (real) +- cj/org-capture--display-sole-window (real) +- display-buffer / display-buffer-alist (real) + +Validates the popup frame ends with one window showing the CAPTURE buffer." + ;; The batch frame is auto-named (\"F1\"), which cannot be restored by name + ;; (\"F<num> usurped by Emacs\"); reset to nil to return it to auto-naming, + ;; keeping the test independent of execution order. + (let ((buf (get-buffer-create "CAPTURE-itest"))) + (unwind-protect + (progn + (set-frame-parameter nil 'name "org-capture") + (delete-other-windows) + (display-buffer buf) + (should (= (length (window-list)) 1)) + (should (eq (window-buffer (selected-window)) buf))) + (set-frame-parameter nil 'name nil) + (when (buffer-live-p buf) (kill-buffer buf))))) + +;;; cj/--org-capture-popup-templates (pure subset/retarget) + +(ert-deftest test-org-capture-config-popup-templates-keeps-tbe () + "Normal: only Task, Bug, Event survive, preserving order." + (should (equal (mapcar #'car (cj/--org-capture-popup-templates + test-org-capture-popup--sample-templates "/inbox.org")) + '("t" "b" "e")))) + +(ert-deftest test-org-capture-config-popup-templates-retargets-task-bug () + "Normal: Task and Bug retarget to the inbox \"Inbox\" headline; body + props kept." + (let* ((result (cj/--org-capture-popup-templates + test-org-capture-popup--sample-templates "/inbox.org")) + (task (assoc "t" result)) + (bug (assoc "b" result))) + (should (equal (nth 3 task) '(file+headline "/inbox.org" "Inbox"))) + (should (equal (nth 3 bug) '(file+headline "/inbox.org" "Inbox"))) + (should (equal (nth 4 task) "* TODO %?")) + (should (equal (nth 4 bug) "* TODO [#C] %?")) + (should (memq :prepend task)))) + +(ert-deftest test-org-capture-config-popup-templates-event-unchanged () + "Boundary: Event passes through untouched, schedule-file target and props intact." + (let ((event (assoc "e" (cj/--org-capture-popup-templates + test-org-capture-popup--sample-templates "/inbox.org")))) + (should (equal (nth 3 event) '(file+headline schedule-file "Scheduled Events"))) + (should (memq :prepare-finalize event)))) + +(ert-deftest test-org-capture-config-popup-templates-drops-context-templates () + "Boundary: context-dependent templates (mu4e, link, drill) are dropped." + (let ((result (cj/--org-capture-popup-templates + test-org-capture-popup--sample-templates "/inbox.org"))) + (should-not (assoc "m" result)) + (should-not (assoc "L" result)) + (should-not (assoc "d" result)))) + +(ert-deftest test-org-capture-config-popup-templates-empty () + "Error/Boundary: empty or all-dropped input yields nil without raising." + (should-not (cj/--org-capture-popup-templates nil "/inbox.org")) + (should-not (cj/--org-capture-popup-templates + '(("L" "Link" entry (file+headline f "Inbox") "* %?")) "/inbox.org"))) + +;;; cj/quick-capture (binds the subset; integration with a stubbed org-capture) + +(ert-deftest test-integration-org-capture-quick-capture-binds-subset () + "Integration: cj/quick-capture runs org-capture with only Task/Bug/Event, +Task and Bug retargeted to the inbox. + +Components integrated: +- cj/quick-capture (real) +- cj/--org-capture-popup-templates (real) +- org-capture (MOCKED — records the dynamically-bound templates)" + (let ((org-capture-templates test-org-capture-popup--sample-templates) + captured) + (cl-letf (((symbol-function 'org-capture) + (lambda (&rest _) (setq captured org-capture-templates)))) + (cj/quick-capture)) + (should (equal (mapcar #'car captured) '("t" "b" "e"))) + (should (equal (nth 3 (assoc "t" captured)) (list 'file+headline inbox-file "Inbox"))) + (should (equal (nth 3 (assoc "b" captured)) (list 'file+headline inbox-file "Inbox"))))) + +(ert-deftest test-integration-org-capture-quick-capture-closes-frame-on-abort () + "Integration: when selection aborts (org-capture signals), cj/quick-capture +deletes the popup frame instead of leaving it orphaned. + +Components integrated: +- cj/quick-capture (real) +- org-capture (MOCKED — signals user-error \"Abort\") +- cj/org-capture--delete-popup-frame (MOCKED — records the call)" + (let ((org-capture-templates test-org-capture-popup--sample-templates) + (deleted 0)) + (cl-letf (((symbol-function 'org-capture) + (lambda (&rest _) (user-error "Abort"))) + ((symbol-function 'cj/org-capture--delete-popup-frame) + (lambda () (cl-incf deleted)))) + (cj/quick-capture)) + (should (= deleted 1)))) + +(ert-deftest test-integration-org-capture-quick-capture-closes-frame-on-quit () + "Integration: a C-g (quit) during capture also closes the popup frame." + (let ((org-capture-templates test-org-capture-popup--sample-templates) + (deleted 0)) + (cl-letf (((symbol-function 'org-capture) + (lambda (&rest _) (signal 'quit nil))) + ((symbol-function 'cj/org-capture--delete-popup-frame) + (lambda () (cl-incf deleted)))) + (cj/quick-capture)) + (should (= deleted 1)))) + +(ert-deftest test-integration-org-capture-quick-capture-keeps-frame-on-success () + "Integration: a successful capture (no signal) does NOT delete the frame — +the finalize hook owns that." + (let ((org-capture-templates test-org-capture-popup--sample-templates) + (deleted 0)) + (cl-letf (((symbol-function 'org-capture) (lambda (&rest _) nil)) + ((symbol-function 'cj/org-capture--delete-popup-frame) + (lambda () (cl-incf deleted)))) + (cj/quick-capture)) + (should (= deleted 0)))) + +;;; cj/--org-capture-popup-strip-specials (drop the Customize menu entry) + +(ert-deftest test-org-capture-config-popup-strip-specials-removes-customize () + "Normal: the \"C\" Customize entry is removed, \"q\" Abort kept, order intact." + (should (equal (cj/--org-capture-popup-strip-specials + '(("C" "Customize org-capture-templates") ("q" "Abort"))) + '(("q" "Abort"))))) + +(ert-deftest test-org-capture-config-popup-strip-specials-no-customize () + "Boundary: specials without a \"C\" entry pass through unchanged." + (should (equal (cj/--org-capture-popup-strip-specials '(("q" "Abort"))) + '(("q" "Abort"))))) + +(ert-deftest test-org-capture-config-popup-strip-specials-empty () + "Error/Boundary: nil specials yields nil without raising." + (should-not (cj/--org-capture-popup-strip-specials nil))) + +;;; cj/org-capture--popup-frame-p + +(ert-deftest test-org-capture-config-popup-frame-p () + "Normal/Boundary: true only when the selected frame is named \"org-capture\"." + (cl-letf (((symbol-function 'frame-parameter) (lambda (&rest _) "org-capture"))) + (should (cj/org-capture--popup-frame-p))) + (cl-letf (((symbol-function 'frame-parameter) (lambda (&rest _) "emacs"))) + (should-not (cj/org-capture--popup-frame-p)))) + +;;; cj/org-capture--popup-mks-advice (frame-gated specials stripping) + +(ert-deftest test-org-capture-config-popup-mks-advice-strips-in-popup () + "Integration: in the popup frame, org-mks receives specials without \"C\"." + (let (seen) + (cl-letf (((symbol-function 'cj/org-capture--popup-frame-p) (lambda () t))) + (cj/org-capture--popup-mks-advice + (lambda (_table _title _prompt specials) (setq seen specials)) + nil nil nil '(("C" "Customize org-capture-templates") ("q" "Abort")))) + (should (equal seen '(("q" "Abort")))))) + +(ert-deftest test-org-capture-config-popup-mks-advice-keeps-elsewhere () + "Integration: in a normal frame, org-mks receives the specials untouched." + (let (seen) + (cl-letf (((symbol-function 'cj/org-capture--popup-frame-p) (lambda () nil))) + (cj/org-capture--popup-mks-advice + (lambda (_table _title _prompt specials) (setq seen specials)) + nil nil nil '(("C" "Customize org-capture-templates") ("q" "Abort")))) + (should (equal seen '(("C" "Customize org-capture-templates") ("q" "Abort")))))) + +;;; cj/org-capture--popup-frame (find the popup frame by name) + +(ert-deftest test-org-capture-config-popup-frame-found () + "Normal: returns the live frame whose name is \"org-capture\"." + (cl-letf (((symbol-function 'frame-list) (lambda () '(fa fb fc))) + ((symbol-function 'frame-live-p) (lambda (_f) t)) + ((symbol-function 'frame-parameter) + (lambda (f _p) (if (eq f 'fb) "org-capture" "other")))) + (should (eq (cj/org-capture--popup-frame) 'fb)))) + +(ert-deftest test-org-capture-config-popup-frame-none () + "Boundary: no popup frame present yields nil." + (cl-letf (((symbol-function 'frame-list) (lambda () '(fa fc))) + ((symbol-function 'frame-live-p) (lambda (_f) t)) + ((symbol-function 'frame-parameter) (lambda (_f _p) "other"))) + (should-not (cj/org-capture--popup-frame)))) + +;;; cj/quick-capture targets the popup frame + +(ert-deftest test-integration-org-capture-quick-capture-selects-named-frame () + "Integration: cj/quick-capture selects the \"org-capture\" frame found by name, +not whatever frame happens to be selected (the emacsclient -c focus race)." + (let ((org-capture-templates test-org-capture-popup--sample-templates) + (focused nil)) + (cl-letf (((symbol-function 'cj/org-capture--popup-frame) (lambda () 'popup-frame)) + ((symbol-function 'select-frame-set-input-focus) + (lambda (f) (setq focused f))) + ((symbol-function 'org-capture) (lambda (&rest _) nil))) + (cj/quick-capture)) + (should (eq focused 'popup-frame)))) + +(ert-deftest test-integration-org-capture-quick-capture-no-frame-still-captures () + "Integration: when no popup frame is found, cj/quick-capture skips the focus +call and still runs the capture (no error)." + (let ((org-capture-templates test-org-capture-popup--sample-templates) + (focused 'unset) + (captured nil)) + (cl-letf (((symbol-function 'cj/org-capture--popup-frame) (lambda () nil)) + ((symbol-function 'select-frame-set-input-focus) + (lambda (f) (setq focused f))) + ((symbol-function 'org-capture) (lambda (&rest _) (setq captured t)))) + (cj/quick-capture)) + (should (eq focused 'unset)) + (should captured))) + +(provide 'test-org-capture-config-popup-window) +;;; test-org-capture-config-popup-window.el ends here diff --git a/tests/test-org-drill-config-commands.el b/tests/test-org-drill-config-commands.el index 7d197616..c35bd6cd 100644 --- a/tests/test-org-drill-config-commands.el +++ b/tests/test-org-drill-config-commands.el @@ -71,21 +71,50 @@ ;;; cj/drill-refile -(ert-deftest test-org-drill-refile-sets-targets-and-delegates () - "Normal: drill-refile narrows `org-refile-targets' to current buffer + -`drill-dir', then dispatches to `org-refile' via `call-interactively'." - (let (seen-targets called-fn) - (cl-letf (((symbol-function 'call-interactively) +(ert-deftest test-org-drill-refile-targets-from-validated-helper () + "Normal: drill-refile builds its drill targets from the shared +`cj/--drill-files-or-error' helper, expanded against `drill-dir' — not from +a raw `directory-files' call (so it inherits the helper's dot-file exclusion +and validation)." + (let ((drill-dir "/tmp/cj-drill/") + seen-targets called-fn) + (cl-letf (((symbol-function 'cj/--drill-files-or-error) + (lambda (_dir) '("a.org" "b.org"))) + ;; If the old raw path were still in use it would call + ;; `directory-files'; a sentinel here keeps it from masquerading. + ((symbol-function 'directory-files) + (lambda (&rest _) '("/WRONG/raw.org"))) + ((symbol-function 'call-interactively) (lambda (fn) (setq called-fn fn seen-targets org-refile-targets)))) (cj/drill-refile)) (should (eq called-fn 'org-refile)) - (should seen-targets) - ;; Two entries: (nil :maxlevel . 1) and (drill-dir :maxlevel . 1). (should (= 2 (length seen-targets))) (should (assoc nil seen-targets)) - (should (assoc 'drill-dir seen-targets)))) + (should (equal (car (nth 1 seen-targets)) + '("/tmp/cj-drill/a.org" "/tmp/cj-drill/b.org"))))) + +(ert-deftest test-org-drill-refile-does-not-clobber-global-targets () + "Error: drill-refile let-binds `org-refile-targets'; the session-wide value +survives the call instead of being permanently replaced." + (let ((drill-dir "/tmp/cj-drill/") + (org-refile-targets '((sentinel :maxlevel . 9)))) + (cl-letf (((symbol-function 'cj/--drill-files-or-error) (lambda (_dir) '("a.org"))) + ((symbol-function 'call-interactively) (lambda (_fn) nil))) + (cj/drill-refile)) + (should (equal org-refile-targets '((sentinel :maxlevel . 9)))))) + +(ert-deftest test-org-drill-refile-errors-on-missing-drill-dir () + "Error: a missing or unreadable drill dir signals a clear `user-error' via +the shared validated helper, instead of a low-level error, and never reaches +`org-refile'." + (let ((drill-dir (expand-file-name "cj-drill-nonexistent-XYZ/" + temporary-file-directory)) + (called nil)) + (cl-letf (((symbol-function 'call-interactively) (lambda (_fn) (setq called t)))) + (should-error (cj/drill-refile) :type 'user-error)) + (should-not called))) (provide 'test-org-drill-config-commands) ;;; test-org-drill-config-commands.el ends here diff --git a/tests/test-org-roam-config-dailies-head.el b/tests/test-org-roam-config-dailies-head.el new file mode 100644 index 00000000..631f017c --- /dev/null +++ b/tests/test-org-roam-config-dailies-head.el @@ -0,0 +1,29 @@ +;;; test-org-roam-config-dailies-head.el --- Tests for the dailies template head -*- lexical-binding: t; -*- + +;;; Commentary: +;; `cj/--org-roam-dailies-head' is the head inserted into a new org-roam +;; daily file. #+FILETAGS and #+TITLE must sit on separate lines, or Org +;; never parses the #+TITLE keyword and the FILETAGS value swallows the +;; rest of the line. + +;;; Code: + +(require 'ert) +(require 'testutil-general) + +(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory)) +(require 'org-roam-config) + +(ert-deftest test-org-roam-config-dailies-head-separates-filetags-and-title () + "Boundary: #+FILETAGS and #+TITLE sit on separate lines." + (should (string-match-p "#\\+FILETAGS: Journal\n#\\+TITLE:" + cj/--org-roam-dailies-head)) + ;; And never run together on one line. + (should-not (string-match-p "Journal #\\+TITLE:" cj/--org-roam-dailies-head))) + +(ert-deftest test-org-roam-config-dailies-head-ends-with-newline () + "Boundary: the head ends with a newline so the capture body starts clean." + (should (string-suffix-p "\n" cj/--org-roam-dailies-head))) + +(provide 'test-org-roam-config-dailies-head) +;;; test-org-roam-config-dailies-head.el ends here diff --git a/tests/test-prog-general--electric-pair-angle.el b/tests/test-prog-general--electric-pair-angle.el new file mode 100644 index 00000000..cb33725a --- /dev/null +++ b/tests/test-prog-general--electric-pair-angle.el @@ -0,0 +1,54 @@ +;;; test-prog-general--electric-pair-angle.el --- Angle-bracket pairing inhibit -*- lexical-binding: t; -*- + +;;; Commentary: +;; Tests for cj/--electric-pair-inhibit-angle, which stops electric-pair from +;; pairing "<" into "<>". Craig's yasnippet keys start with "<" (e.g. <cj); +;; auto-pairing the "<" strands a ">" after the expanded snippet, which broke +;; the cj-comment close fence into "#+end_src>". + +;;; Code: + +(require 'ert) +(require 'cl-lib) +(require 'elec-pair) +(require 'org) +(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory)) +(require 'prog-general) + +;;; cj/--electric-pair-inhibit-angle + +(ert-deftest test-prog-general-electric-pair-inhibit-angle-open () + "Normal: the open angle bracket is inhibited." + (should (cj/--electric-pair-inhibit-angle ?<))) + +(ert-deftest test-prog-general-electric-pair-inhibit-angle-delegates () + "Boundary: any other character defers to electric-pair-default-inhibit." + (cl-letf (((symbol-function 'electric-pair-default-inhibit) + (lambda (_c) 'delegated))) + (should (eq (cj/--electric-pair-inhibit-angle ?a) 'delegated)) + (should (eq (cj/--electric-pair-inhibit-angle ?\() 'delegated)))) + +(ert-deftest test-prog-general-electric-pair-predicate-installed () + "Normal: prog-general installs the predicate as the global value." + (should (eq electric-pair-inhibit-predicate #'cj/--electric-pair-inhibit-angle))) + +;;; Integration — the actual pairing behavior + +(ert-deftest test-integration-prog-general-angle-not-paired-in-org () + "Integration: in an org buffer (where < has paren syntax), typing < with the +inhibit predicate active inserts just <, not <>. + +Components integrated: +- cj/--electric-pair-inhibit-angle (real) +- electric-pair-local-mode / self-insert-command (real) +- org-mode syntax table (real — gives < paren syntax)" + (with-temp-buffer + (org-mode) + (electric-pair-local-mode 1) + (setq-local electric-pair-inhibit-predicate #'cj/--electric-pair-inhibit-angle) + (let ((last-command-event ?<)) + (call-interactively #'self-insert-command)) + (should (equal (buffer-substring-no-properties (point-min) (point-max)) "<")))) + +(provide 'test-prog-general--electric-pair-angle) +;;; test-prog-general--electric-pair-angle.el ends here diff --git a/tests/test-reconcile--find-git-repos.el b/tests/test-reconcile--find-git-repos.el index e065fca9..c6a190a1 100644 --- a/tests/test-reconcile--find-git-repos.el +++ b/tests/test-reconcile--find-git-repos.el @@ -81,6 +81,15 @@ (should (= (length repos) 1)) (should (string-suffix-p "visible-repo" (car repos)))))) +(ert-deftest test-find-git-repos-boundary-dotted-repo-name-found () + "Boundary: a repo whose directory name contains a dot (e.g. mcp.el) is +discovered. Regression for the `^[^.]+$' filter that matched only dot-free +names and silently skipped dotted repos like mcp.el / capture.el." + (reconcile-test-with-temp-dirs + ("mcp.el/.git/" "capture.el/.git/" "plain-repo/.git/") + (let ((repos (cj/find-git-repos test-root))) + (should (= (length repos) 3))))) + (ert-deftest test-find-git-repos-boundary-prunes-heavy-directories () "Skips generated/heavy directories while discovering repos." (reconcile-test-with-temp-dirs diff --git a/tests/test-selection-framework--consult-line-or-repeat.el b/tests/test-selection-framework--consult-line-or-repeat.el index fcaddcfd..66f5b172 100644 --- a/tests/test-selection-framework--consult-line-or-repeat.el +++ b/tests/test-selection-framework--consult-line-or-repeat.el @@ -64,5 +64,11 @@ "Normal: `cj/consult-line-or-repeat' is an interactive command." (should (commandp #'cj/consult-line-or-repeat))) +(ert-deftest test-selection-framework-vertico-repeat-save-on-minibuffer-setup () + "Normal: loading the module registers `vertico-repeat-save' on +`minibuffer-setup-hook'. Without it `vertico-repeat' has no saved session +and the second C-s signals \"No Vertico session\"." + (should (memq 'vertico-repeat-save minibuffer-setup-hook))) + (provide 'test-selection-framework--consult-line-or-repeat) ;;; test-selection-framework--consult-line-or-repeat.el ends here diff --git a/tests/test-system-lib-confirm-strong.el b/tests/test-system-lib-confirm-strong.el new file mode 100644 index 00000000..26c00822 --- /dev/null +++ b/tests/test-system-lib-confirm-strong.el @@ -0,0 +1,37 @@ +;;; test-system-lib-confirm-strong.el --- Tests for cj/confirm-strong -*- lexical-binding: t; -*- + +;;; Commentary: +;; ERT tests for `cj/confirm-strong', the typed-"yes" confirmation used for +;; irreversible actions. The behavior under test is the long-form guarantee: +;; the prompt demands a typed yes/no even when the global single-key default +;; (`use-short-answers') is in effect. + +;;; Code: + +(require 'ert) +(require 'cl-lib) +(require 'system-lib) + +(ert-deftest test-system-lib-confirm-strong-returns-t-on-yes () + "Normal: passes a t answer through from `yes-or-no-p'." + (cl-letf (((symbol-function 'yes-or-no-p) (lambda (&rest _) t))) + (should (eq (cj/confirm-strong "Really? ") t)))) + +(ert-deftest test-system-lib-confirm-strong-returns-nil-on-no () + "Normal: passes a nil answer through from `yes-or-no-p'." + (cl-letf (((symbol-function 'yes-or-no-p) (lambda (&rest _) nil))) + (should (eq (cj/confirm-strong "Really? ") nil)))) + +(ert-deftest test-system-lib-confirm-strong-forces-long-form () + "Boundary: binds `use-short-answers' to nil for the call even when it is +globally t, so the irreversible prompt requires a typed yes/no regardless of +the single-key default." + (let ((use-short-answers t) + (seen 'unset)) + (cl-letf (((symbol-function 'yes-or-no-p) + (lambda (&rest _) (setq seen use-short-answers) t))) + (cj/confirm-strong "Really? ") + (should (eq seen nil))))) + +(provide 'test-system-lib-confirm-strong) +;;; test-system-lib-confirm-strong.el ends here diff --git a/tests/test-ui-navigation-split-follow-undo-kill.el b/tests/test-ui-navigation-split-follow-undo-kill.el index 74c1e2fc..8e390074 100644 --- a/tests/test-ui-navigation-split-follow-undo-kill.el +++ b/tests/test-ui-navigation-split-follow-undo-kill.el @@ -54,8 +54,9 @@ ;;; cj/undo-kill-buffer -(ert-deftest test-ui-navigation-undo-kill-buffer-opens-most-recent () - "Normal: with no arg, opens the head of recentf-list that isn't currently visited." +(ert-deftest test-ui-navigation-undo-kill-buffer-no-prefix-opens-most-recent () + "Normal: no prefix (arg=1, the value `\"p\"' yields) opens the most-recent +non-visited entry, not the second." (let ((opened nil) (recentf-mode t) (recentf-list '("/tmp/dead.org" "/tmp/alive.txt"))) @@ -71,12 +72,12 @@ ((symbol-function 'find-file) (lambda (f) (setq opened f)))) (unwind-protect - (cj/undo-kill-buffer 0) + (cj/undo-kill-buffer 1) (when (get-buffer "*test-alive*") (kill-buffer "*test-alive*")))) (should (equal opened "/tmp/dead.org")))) -(ert-deftest test-ui-navigation-undo-kill-buffer-honors-numeric-arg () - "Normal: with N=1, opens the second non-visited entry from recentf-list." +(ert-deftest test-ui-navigation-undo-kill-buffer-numeric-arg-is-one-based () + "Normal: a numeric prefix is 1-based — N=2 opens the second non-visited entry." (let ((opened nil) (recentf-mode t) (recentf-list '("/tmp/a.org" "/tmp/b.org" "/tmp/c.org"))) @@ -85,10 +86,7 @@ ((symbol-function 'buffer-list) (lambda (&rest _) nil)) ((symbol-function 'find-file) (lambda (f) (setq opened f)))) - ;; cj/undo-kill-buffer takes a prefix `arg' and indexes into the list - ;; with `(nth arg ...)` when arg is non-nil. Passing 1 grabs the 2nd - ;; entry. - (cj/undo-kill-buffer 1)) + (cj/undo-kill-buffer 2)) (should (equal opened "/tmp/b.org")))) (ert-deftest test-ui-navigation-undo-kill-buffer-no-op-when-list-empty () @@ -104,5 +102,18 @@ (cj/undo-kill-buffer 0)) (should-not opened))) +(ert-deftest test-ui-navigation-undo-kill-buffer-out-of-range-arg-errors () + "Error: a prefix larger than the killed-file list signals a clear user-error, +not a wrong-type-argument from find-file on nil." + (let ((opened nil) + (recentf-mode t) + (recentf-list '("/tmp/a.org"))) + (cl-letf (((symbol-function 'require) (lambda (&rest _) t)) + ((symbol-function 'recentf-mode) (lambda (&rest _) t)) + ((symbol-function 'buffer-list) (lambda (&rest _) nil)) + ((symbol-function 'find-file) (lambda (f) (setq opened f)))) + (should-error (cj/undo-kill-buffer 5) :type 'user-error)) + (should-not opened))) + (provide 'test-ui-navigation-split-follow-undo-kill) ;;; test-ui-navigation-split-follow-undo-kill.el ends here |
