From a7cc492131361195ad08d433c64c7c76b255bc8c Mon Sep 17 00:00:00 2001 From: Craig Jennings Date: Sun, 10 May 2026 12:19:17 -0500 Subject: test(vterm): cover the copy-mode exit chain end-to-end The unit tests for the cursor-restoration hook only exercised the helper in isolation. The real integration -- toggling the minor mode and watching the hook fire as part of the chain -- wasn't covered. If `vterm-copy-mode-done' or `cj/vterm-copy-mode-cancel' broke their exit semantics (or our hook stopped firing on `vterm-copy-mode-hook'), the unit tests would still pass but the cursor would stay stuck on the bar in real use. Add five integration tests that toggle the actual minor mode through stubbed `vterm--enter-copy-mode' / `vterm--exit-copy-mode' (so we don't need a live vterm process) and assert the cursor moves through the full lifecycle: nil -> bar on enter, bar -> killed-local on exit. Cover all three exit paths Craig hits in normal use: - vterm-copy-mode -1 directly (the toggle) - vterm-copy-mode-done with an active region (M-w / RET) - vterm-copy-mode-done with no region (line-selection branch) - cj/vterm-copy-mode-cancel (C-g / ) Plus a multi-cycle test so a regression in `kill-local-variable' handling shows up. --- tests/test-vterm-copy-mode-cursor.el | 93 ++++++++++++++++++++++++++++++++++++ 1 file changed, 93 insertions(+) diff --git a/tests/test-vterm-copy-mode-cursor.el b/tests/test-vterm-copy-mode-cursor.el index ee72a0bb..85fc2897 100644 --- a/tests/test-vterm-copy-mode-cursor.el +++ b/tests/test-vterm-copy-mode-cursor.el @@ -12,10 +12,26 @@ ;;; Code: (require 'ert) +(require 'cl-lib) +(require 'package) +(setq package-user-dir (expand-file-name "elpa" user-emacs-directory)) +(package-initialize) (add-to-list 'load-path (expand-file-name "modules" user-emacs-directory)) (defvar vterm-copy-mode nil) (require 'vterm-config) +(require 'vterm) + +(defmacro test-vterm-copy-mode-cursor--in-fake-vterm-buffer (&rest body) + "Run BODY in a temp buffer pretending to be a live vterm. +Stubs `vterm--enter-copy-mode' and `vterm--exit-copy-mode' so toggling +`vterm-copy-mode' doesn't try to talk to a real vterm process." + (declare (indent 0)) + `(cl-letf (((symbol-function 'vterm--enter-copy-mode) #'ignore) + ((symbol-function 'vterm--exit-copy-mode) #'ignore)) + (with-temp-buffer + (setq-local major-mode 'vterm-mode) + ,@body))) (ert-deftest test-vterm-copy-mode-cursor-restored-on-enter () "Normal: entering copy-mode with cursor-type nil sets a visible cursor." @@ -47,5 +63,82 @@ (should (memq #'cj/--vterm-copy-mode-restore-cursor vterm-copy-mode-hook))) +(ert-deftest test-vterm-copy-mode-cursor-end-to-end-via-mode-toggle () + "Normal: toggling `vterm-copy-mode' on then off via the real minor mode +command produces the visible cursor on entry and removes the override on +exit. This exercises the full path -- mode body, hook registration, our +restore function -- not just the helper in isolation." + (test-vterm-copy-mode-cursor--in-fake-vterm-buffer + (setq-local cursor-type nil) + ;; Enter copy-mode through the actual minor-mode command, not by + ;; let-binding the variable. This fires `vterm-copy-mode-hook'. + (vterm-copy-mode 1) + (should (eq vterm-copy-mode t)) + (should (equal cursor-type '(bar . 3))) + ;; Exit through the same path. + (vterm-copy-mode -1) + (should (eq vterm-copy-mode nil)) + (should-not (local-variable-p 'cursor-type)))) + +(ert-deftest test-vterm-copy-mode-cursor-end-to-end-via-copy-done () + "Normal: `vterm-copy-mode-done' (M-w / RET binding) toggles copy-mode +off and triggers cursor restoration. This is the path the user takes +most often -- copy and exit in one keystroke." + (test-vterm-copy-mode-cursor--in-fake-vterm-buffer + (setq-local cursor-type nil) + (vterm-copy-mode 1) + (should (eq vterm-copy-mode t)) + (should (equal cursor-type '(bar . 3))) + (insert "selectable text on this line") + (set-mark (point-min)) + (goto-char (point-max)) + (vterm-copy-mode-done nil) + (should (eq vterm-copy-mode nil)) + (should-not (local-variable-p 'cursor-type)))) + +(ert-deftest test-vterm-copy-mode-cursor-end-to-end-via-cancel () + "Normal: `cj/vterm-copy-mode-cancel' (C-g / binding) toggles +copy-mode off and triggers cursor restoration even when no region was +selected -- the cancel path skips the kill-ring step entirely." + (test-vterm-copy-mode-cursor--in-fake-vterm-buffer + (setq-local cursor-type nil) + (vterm-copy-mode 1) + (should (equal cursor-type '(bar . 3))) + (cj/vterm-copy-mode-cancel) + (should (eq vterm-copy-mode nil)) + (should-not (local-variable-p 'cursor-type)))) + +(ert-deftest test-vterm-copy-mode-cursor-end-to-end-via-copy-done-no-region () + "Boundary: `vterm-copy-mode-done' called with no active region falls +into its line-selection branch. The branch calls vterm-internal +helpers that aren't safe in a fake buffer, so stub them to point-min / +point-max. The exit-and-fire-hook chain at the function's tail must +still run; cursor restoration must still happen." + (cl-letf (((symbol-function 'vterm--get-beginning-of-line) + (lambda (&rest _) (point-min))) + ((symbol-function 'vterm--get-end-of-line) + (lambda (&rest _) (point-max)))) + (test-vterm-copy-mode-cursor--in-fake-vterm-buffer + (insert "line content") + (setq-local cursor-type nil) + (vterm-copy-mode 1) + (should (equal cursor-type '(bar . 3))) + (deactivate-mark) + (should-not (use-region-p)) + (vterm-copy-mode-done nil) + (should (eq vterm-copy-mode nil)) + (should-not (local-variable-p 'cursor-type))))) + +(ert-deftest test-vterm-copy-mode-cursor-survives-multiple-cycles () + "Boundary: enter/exit/enter/exit cycles don't accumulate buffer-local +state. The cursor goes back and forth cleanly." + (test-vterm-copy-mode-cursor--in-fake-vterm-buffer + (setq-local cursor-type nil) + (dotimes (_ 3) + (vterm-copy-mode 1) + (should (equal cursor-type '(bar . 3))) + (vterm-copy-mode -1) + (should-not (local-variable-p 'cursor-type))))) + (provide 'test-vterm-copy-mode-cursor) ;;; test-vterm-copy-mode-cursor.el ends here -- cgit v1.2.3