ert-play-keys function

* lisp/emacs-lisp/ert-x.el (ert-play-keys): new defun.

* test/lisp/simple-tests.el (undo-test-kill-c-a-then-undo)
(undo-test-point-after-forward-kill): Use new function
`ert-play-keys' and `(ert-with-test-buffer (:selected t) ...)'
rather (with-temp-buffer (switch-to-buffer (current-buffer) ...)'.

* test/lisp/erc/erc-scenarios-spelling.el
(erc-scenarios-spelling--auto-correct): Use new function
`ert-play-keys' and `(ert-with-buffer-selected ...)' rather than
`execute-kbd-macro' and `(with-current-buffer
... (set-window-buffer nil (current-buffer) ...)'.

* doc/misc/ert.texi (Helper Functions): Document ert-play-keys,
and differences between ert-simulate-command, ert-simulate-keys
& ert-play-keys.

* test/lisp/emacs-lisp/ert-x-tests.el
(ert-x-tests-play-keys)
(ert-x-tests-simulate-command, ert-x-tests-simulate-keys): New
tests.
This commit is contained in:
Vincent Belaïche
2026-03-29 06:17:55 +02:00
parent 1c4c9d759a
commit 5b6fc8ebfc
6 changed files with 205 additions and 37 deletions

View File

@@ -1132,6 +1132,12 @@ This has the same effect as combining @code{ert-with-test-buffer} with
(ert-with-test-buffer (:name "global" :selected t)
@dots{}))
@end lisp
@findex ert-play-keys
The @var{select-form} shall be set non-nil when @var{body} contains some
call to @code{ert-play-keys} to generate programmatically user input
events inserting text into the test buffer, or starting commands acting
on the test buffer.
@end defmac
@defmac ert-with-buffer-selected (buffer &body body)
@@ -1154,6 +1160,13 @@ value is the last form in @var{body}. Example:
@end lisp
This displays a temporary buffer like @file{ *temp*-739785*}.
@findex ert-play-keys
One of the use of @code{ert-with-buffer-selected} is to set the buffer
to which user input events generated programmatically by one or more
calls to @code{ert-play-keys} are targetted, which is needed when those
events are supposed to insert text into this buffer, or start commands
acting on it.
@end defmac
@subsection Protecting buffers
@@ -1348,7 +1361,14 @@ symbol and the rest are arguments to the command. Example:
@strong{Note}: Since the command is not called by
@code{call-interactively}, a test for @code{(called-interactively-p
'interactive)} in the command will fail.
@var{kind})} in the command will fail for whatever @var{kind}.@*
Function @code{ert-play-keys} may be used instead to start a command if
you need the predicate @code{(called-interactively-p @var{kind})} tested
within the command body to return @code{t} for @var{kind} @code{any},
note however that it will still be @code{nil} too for @var{kind}
@code{interactive} since @code{ert-play-keys} uses keyboard macros under
the hood, and that @code{ert-play-keys} needs selecting the buffer of
interest.
@end defun
@defmac ert-simulate-keys (keys &rest body)
@@ -1362,8 +1382,76 @@ vector. Examples:
(ert-simulate-keys (kbd "#fake C-m C-a C-k C-m") @dots{})
(ert-simulate-keys [?b ?2 return] @dots{})
@end lisp
@c @findex ert-play-keys
To generate input event for inserting some text into a buffer, or
calling some interactive command, see rather function
@code{ert-play-keys}.
@end defmac
@defun ert-play-keys (keys)
Generate programmatically user input events.
@c @findex ert-simulate-keys
Contrary to @code{ert-simulate-keys}, these events are not intended to be
consumed by functions reading input, like @code{read-from-minibuffer},
but are consumed by the command loop which typically will process them
to start interactive commands or insert text into the selected buffer.
So, before calling @code{ert-play-keys} you generally need to select the
buffer to which input events are intended to insert text or call a
command. Do this by passing a non-nil @code{:selected} flag to
@code{ert-with-test-buffer} if the buffer was created this way, or use
the @code{ert-with-buffer-selected} macro.
@c @findex ert-simulate-command
Contrary to @code{ert-simulate-command}, when @code{ert-play-keys}
generates events starting a command you cannot get the command return
value. On the other hand, @code{(called-interactively-p 'any)} tested in
the command body will be @code{t}, but not @code{(called-interactively-p
'interactive)} as @code{ert-play-keys} does not a true interactive call,
but uses a keyboard macro under the hood. Another difference is that,
contrary to @code{ert-simulate-command}, @code{ert-play-keys} needs to
select the buffer on which the command acts for the input events to
reach it.
In this example a test buffer is created and selected, then
@code{ert-play-keys} sets the mark, inserts text @samp{n'importe quoi}
and kills it, then the test checks that the killed text is in the kill
ring and the test buffer is empty, then a second @code{ert-play-keys}
call yanks again the killed text, and finally the test checks the test
buffer contains @samp{n'importe quoi}:
@lisp
(ert-deftest ert-example-kill&yank ()
"Test kill and yank."
(ert-with-test-buffer (:selected t)
(ert-play-keys "C-SPC n'importe SPC quoi C-w")
(should (string= "n'importe quoi" (car kill-ring)))
(should (string= "" (buffer-substring (point-min) (point-max))))
(ert-play-keys "C-y")
(should (string= "n'importe quoi"
(buffer-substring (point-min) (point-max))))))
@end lisp
@noindent
Write input events as above with a string in the input format used by
@code{key-parse}, or directly in the internal Emacs representation like
here which is otherwise the same test as above:
@lisp
(ert-deftest ert-example-kill&yank ()
"Test kill and yank."
(ert-with-test-buffer (:selected t)
(ert-play-keys (vconcat [ ?\C- ] "n'importe quoi" [ ?\C-w]))
(should (string= "n'importe quoi" (car kill-ring)))
(should (string= "" (buffer-substring (point-min) (point-max))))
(ert-play-keys [ ?\C-y ])
(should (string= "n'importe quoi"
(buffer-substring (point-min) (point-max))))))
@end lisp
@end defun
@defun ert-filter-string (s &rest regexps)
This function returns a copy of string @var{s} with all matches of
@var{regexps} removed. Elements of @var{regexps} may also be

View File

@@ -416,6 +416,19 @@ The same keyword arguments are supported as in
(format "/mock::%s" temporary-file-directory))))
"Temporary directory for remote file tests.")
(defun ert-play-keys (keys)
"Play the key sequence KEYS as if it was user input.
KEYS shall have the same format as in a call to function `kmacro'.
This macro should be expanded within the body of
`ert-with-buffer-selected' to select a buffer when keys KEYS start
commands acting on this buffer, or within the body of
`ert-with-test-buffer' used with `:selected' flag set."
(funcall
(kmacro keys)))
;;;; Obsolete

View File

@@ -3125,14 +3125,13 @@ The return value is the last form in BODY."
If BUFFER-OR-NAME is nil, the current buffer is used.
The buffer is made the current buffer, and the temporary window
becomes the `selected-window', before BODY is evaluated. The
modification hooks `before-change-functions' and
`after-change-functions' are not inhibited during the evaluation
of BODY, which makes it easier to use `execute-kbd-macro' to
simulate user interaction. The window configuration is restored
before returning, even if BODY exits nonlocally. The return
value is the last form in BODY."
The buffer is made the current buffer, and the temporary window becomes
the `selected-window', before BODY is evaluated. The modification hooks
`before-change-functions' and `after-change-functions' are not inhibited
during the evaluation of BODY, which makes it easier to use
`ert-play-keys' to simulate user sending input events. The window
configuration is restored before returning, even if BODY exits
nonlocally. The return value is the last form in BODY."
(declare (debug (form body)) (indent 1))
`(save-window-excursion
(with-current-buffer (or ,buffer-or-name (current-buffer))

View File

@@ -293,6 +293,77 @@ desired effect."
(should-error
(ert-with-temp-directory dir :text "foo" nil)))
(ert-deftest ert-x-tests-play-keys ()
"Test `ert-play-keys'.
Send one symbolic event, some inserted text, and some key event to the
test buffer, and check all of them are processed."
(ert-with-test-buffer (:selected t)
(let (verdict-event verdict-key verdict-pre-command-hook verdict-post-command-hook)
(let ((pre-command-hook (lambda () (setq verdict-pre-command-hook t)))
(post-command-hook (lambda () (setq verdict-post-command-hook t)))
(map (let ((map (make-sparse-keymap)))
(define-key map [event]
(lambda ()
(interactive)
(setq verdict-event
(list t
(called-interactively-p 'any)
(called-interactively-p 'interactive)))))
(define-key map [?$]
(lambda ()
(interactive)
(setq verdict-key
(list t
(called-interactively-p 'any)
(called-interactively-p 'interactive)))))
map)))
(let ((minor-mode-map-alist (cons (cons t map) minor-mode-map-alist)))
(ert-play-keys (vconcat [event] "n'importe $quoi"))))
(should (equal verdict-event '(t t nil)))
(should (equal verdict-key '(t t nil)))
(should (eq verdict-pre-command-hook t))
(should (eq verdict-post-command-hook t)))
(should (string= "n'importe quoi"
(buffer-substring (point-min) (point-max))))))
(ert-deftest ert-x-tests-simulate-command ()
"Test `ert-simulate-command'."
(ert-with-test-buffer ()
(let (verdict-interactive verdict-pre-command-hook verdict-post-command-hook)
(let ((pre-command-hook (lambda () (setq verdict-pre-command-hook t)))
(post-command-hook (lambda () (setq verdict-post-command-hook t))))
(should (eq (ert-simulate-command
(list
(lambda (x)
(interactive (list "un rien"))
(insert x)
(setq verdict-interactive (list t
(called-interactively-p 'any)
(called-interactively-p 'interactive)))
:ok)
"n'importe quoi"))
:ok)))
(should (equal verdict-interactive '(t nil nil)))
(should (eq verdict-pre-command-hook t))
(should (eq verdict-post-command-hook t)))
(should (string= "n'importe quoi"
(buffer-substring (point-min) (point-max))))))
(ert-deftest ert-x-tests-simulate-keys ()
"Test `ert-simulate-keys'."
(ert-with-test-buffer ()
(let* ((map (let ((map (make-sparse-keymap)))
(define-key map [?b]
(lambda ()
(interactive)
(insert "r"))) map))
(minor-mode-map-alist (cons (cons t map) minor-mode-map-alist)))
(ert-simulate-keys
(listify-key-sequence "un bien\nn'importe quoi")
(should (string= (read-from-minibuffer "Please enter something: ") "un rien")))
(should (string= "" (buffer-substring (point-min) (point-max)))))))
(provide 'ert-x-tests)
;;; ert-x-tests.el ends here

View File

@@ -61,14 +61,13 @@
(should erc-spelling-mode)
(should flyspell-mode)))
(with-current-buffer (erc-d-t-wait-for 10 (get-buffer "#chan"))
(ert-with-buffer-selected (erc-d-t-wait-for 10 (get-buffer "#chan"))
(should erc-spelling-mode)
(should flyspell-mode)
(funcall expect 10 "<alice> tester, welcome!")
;; Insert a command with one misspelled word.
(set-window-buffer nil (current-buffer))
(execute-kbd-macro "\M->/AMSG an/dor /gmsg one fsbot two frob my shoe")
(ert-play-keys (vconcat [?\M->] "/AMSG an/dor /gmsg one fsbot two frob my shoe"))
(funcall expect 10 "shoe")
(let* ((ovs (overlays-in erc-input-marker (point)))
@@ -90,7 +89,7 @@
;; Depending on the machine, this should become something
;; like: "/AMSG an/dor /gmsg one fsbot two Rob my shoe".
(execute-kbd-macro (key-parse "M-TAB"))
(ert-play-keys "M-TAB")
(should (equal (overlays-in erc-input-marker (point-max))
(list ov1)))))

View File

@@ -21,7 +21,7 @@
;;; Code:
(require 'ert)
(require 'ert-x)
(eval-when-compile (require 'cl-lib))
(defun simple-test--buffer-substrings ()
@@ -730,14 +730,13 @@ See bug#35036."
;; Test for a regression introduced by undo-auto--boundaries changes.
;; https://lists.gnu.org/r/emacs-devel/2015-11/msg01652.html
(defun undo-test-kill-c-a-then-undo ()
(with-temp-buffer
(switch-to-buffer (current-buffer))
(ert-with-test-buffer (:selected t)
(setq buffer-undo-list nil)
(insert "a\nb\nc\n")
(goto-char (point-max))
;; We use a keyboard macro because it adds undo events in the same
;; way as if a user were involved.
(funcall (kmacro [left
(ert-play-keys [left
;; Delete "c"
backspace
left left left
@@ -745,21 +744,20 @@ See bug#35036."
backspace
;; C-/ or undo
?\C-/
]))
])
(point)))
(defun undo-test-point-after-forward-kill ()
(with-temp-buffer
(switch-to-buffer (current-buffer))
(ert-with-test-buffer (:selected t)
(setq buffer-undo-list nil)
(insert "kill word forward")
;; Move to word "word".
(goto-char 6)
(funcall (kmacro [;; kill-word
(ert-play-keys [;; kill-word
C-delete
;; undo
?\C-/
]))
])
(point)))
(ert-deftest undo-point-in-wrong-place ()