Subject: bug#62834: 30.0.50; ERC 5.6: Make erc-button navigation more flexible

From: "J.P." <[email-masked]>
Date: Fri, 14 Apr 2023 07:05:21 -0700

Tags: patch

This is a followup to recent changes introduced by

  bug#60954: 30.0.50; ERC >5.5: Stop requiring erc-goodies in erc.el

which moved the site of various top-level keybindings, such as
erc-button's <backtab>, to module-setup code. However, those changes
neglected to update `erc-button-{next,previous}' to work with the new
"merged" option of `erc-fill-wrap', which was added in:

  bug#60936: 30.0.50; ERC >5.5: Add erc-fill style based on visual-line-mode

While revisiting these commands, I also thought it helpful to slightly
alter the behavior of `erc-button-previous'. The original always moved
point to the end of the previous button, whereas the new version lands
you at the start. This might cause some churn for those using the
command in lisp code, but I think the UX improvement is likely worth it.
(FWIW, you can obtain the old behavior by removing an item from a hook.)

Beyond this, I've also freed `erc-button-previous' so that it's no
longer confined to the read-only message area but is instead allowed to
jump backward from the input area through the prompt. I've also re-bound
<TAB> in all ERC buffers to a new wrapper that only calls
`completion-at-point' from the input area and otherwise runs a hook for
modules to use as needed. As a consequence, I've deprecated
`erc-button-next-function', which no longer serves an obvious purpose.
Questions and comments welcome, as always.

Thanks.

In GNU Emacs 30.0.50 (build 1, x86_64-pc-linux-gnu, GTK+ Version
 3.24.35, cairo version 1.17.6) of 2023-04-12 built on localhost
Repository revision: 861cf3a5c9d2081d811dcfc2c5ce5357f3dc44d4
Repository branch: master
Windowing system distributor 'The X.Org Foundation', version 11.0.12014000
System Description: Fedora Linux 36 (Workstation Edition)

Configured using:
 'configure --enable-check-lisp-object-type --enable-checking=yes,glyphs
 'CFLAGS=-O0 -g3'
 PKG_CONFIG_PATH=:/usr/lib64/pkgconfig:/usr/share/pkgconfig'

Configured features:
ACL CAIRO DBUS FREETYPE GIF GLIB GMP GNUTLS GPM GSETTINGS HARFBUZZ JPEG
JSON LCMS2 LIBOTF LIBSELINUX LIBSYSTEMD LIBXML2 M17N_FLT MODULES NOTIFY
INOTIFY PDUMPER PNG RSVG SECCOMP SOUND SQLITE3 THREADS TIFF
TOOLKIT_SCROLL_BARS WEBP X11 XDBE XIM XINPUT2 XPM GTK3 ZLIB

Important settings:
  value of $LANG: en_US.UTF-8
  value of $XMODIFIERS: @im=ibus
  locale-coding-system: utf-8-unix

Major mode: Lisp Interaction

Minor modes in effect:
  tooltip-mode: t
  global-eldoc-mode: t
  eldoc-mode: t
  show-paren-mode: t
  electric-indent-mode: t
  mouse-wheel-mode: t
  tool-bar-mode: t
  menu-bar-mode: t
  file-name-shadow-mode: t
  global-font-lock-mode: t
  font-lock-mode: t
  blink-cursor-mode: t
  line-number-mode: t
  indent-tabs-mode: t
  transient-mark-mode: t
  auto-composition-mode: t
  auto-encryption-mode: t
  auto-compression-mode: t

Load-path shadows:
None found.

Features:
(shadow sort mail-extr emacsbug message mailcap yank-media puny dired
dired-loaddefs rfc822 mml mml-sec epa derived epg rfc6068 epg-config
gnus-util text-property-search time-date mm-decode mm-bodies mm-encode
mail-parse rfc2231 mailabbrev gmm-utils mailheader sendmail rfc2047
rfc2045 ietf-drums mm-util mail-prsvr mail-utils erc auth-source cl-seq
eieio eieio-core cl-macs password-cache json subr-x map format-spec
cl-loaddefs cl-lib erc-backend erc-networks byte-opt gv bytecomp
byte-compile erc-common erc-compat erc-loaddefs rmc iso-transl tooltip
cconv eldoc paren electric uniquify ediff-hook vc-hooks lisp-float-type
elisp-mode mwheel term/x-win x-win term/common-win x-dnd tool-bar dnd
fontset image regexp-opt fringe tabulated-list replace newcomment
text-mode lisp-mode prog-mode register page tab-bar menu-bar rfn-eshadow
isearch easymenu timer select scroll-bar mouse jit-lock font-lock syntax
font-core term/tty-colors frame minibuffer nadvice seq simple cl-generic
indonesian philippine cham georgian utf-8-lang misc-lang vietnamese
tibetan thai tai-viet lao korean japanese eucjp-ms cp51932 hebrew greek
romanian slovak czech european ethiopic indian cyrillic chinese
composite emoji-zwj charscript charprop case-table epa-hook
jka-cmpr-hook help abbrev obarray oclosure cl-preloaded button loaddefs
theme-loaddefs faces cus-face macroexp files window text-properties
overlay sha1 md5 base64 format env code-pages mule custom widget keymap
hashtable-print-readable backquote threads dbusbind inotify lcms2
dynamic-setting system-font-setting font-render-setting cairo
move-toolbar gtk x-toolkit xinput2 x multi-tty make-network-process
emacs)

Memory information:
((conses 16 63110 9012)
 (symbols 48 8532 0)
 (strings 32 23158 1921)
 (string-bytes 1 669049)
 (vectors 16 14968)
 (vector-slots 8 206726 6655)
 (floats 8 24 39)
 (intervals 56 231 0)
 (buffers 976 10))

>From 54820313783665bb9a9241594511a8cf3589747f Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <[email-masked]>
Date: Mon, 10 Apr 2023 17:58:05 -0700
Subject: [PATCH] [5.6] Simplify erc-button movement commands

* etc/ERC-NEWS: Mention TAB being bound to new command `erc-tab'.
* lisp/erc/erc-button.el (erc-button-mode, erc-button-enable,
erc-button-disable): Add and remove `erc-button-next' to
`erc--tab-functions' hook, which is tantamount to binding the command
in the read-only area of an ERC buffer.
(erc-button-next-function): Deprecate and remove from client code path
because this module doesn't concern itself with prompt input and thus
no longer needs to conform to the `completion-at-point-functions'
interface.
(erc-button--prev-next-predicate-functions): New variable, a hook to
determine whether to continue searching for a button.  Other modules
should utilize this as needed.
(erc-button--end-of-button-p): Add function to serve as default value
for `erc-button--continue-predicate'.
(erc--button-next): Add generalized button-movement function.
(erc-button-next, erc-button-previous): Make `erc-button-previous'
behave more predictably by having it land at the beginning of buttons.
And remove roundabout appeal to HOF in `erc-button-next'.
(erc-button-previous-of-nick): New command to jump to previous
appearance of nick at point.
* lisp/erc/erc-fill.el (erc-fill-wrap, erc-fill-wrap-enable,
erc-fill-wrap-disable): Add and remove merge-related hookee from
`erc-button--prev-next-predicate-functions'.
* lisp/erc/erc.el (erc-mode-map): Bind `erc-tab' to TAB.
(erc--tab-functions, erc-tab): Add new command and hook to serve as
unified dispatch for TAB-related operations.  It calls `c-a-p' in the
input area and defers to module code in the read-only message area.
* test/lisp/erc/erc-button-tests.el: New file.
* test/lisp/erc/erc-fill-tests.el (erc-fill-tests--wrap-populate): Run
finalizer for transient keymap timer.
* test/lisp/erce/erc-tests.el
(erc-button--display-error-notice-with-keys): Move to new dedicated
test file for erc-button and fix expected behavior of
`erc-button-previous'.  (Bug#60954.)
---
 etc/ERC-NEWS                      |  11 +-
 lisp/erc/erc-button.el            |  93 ++++++++++++----
 lisp/erc/erc-fill.el              |  10 +-
 lisp/erc/erc.el                   |  15 ++-
 test/lisp/erc/erc-button-tests.el | 177 ++++++++++++++++++++++++++++++
 test/lisp/erc/erc-fill-tests.el   |   2 +
 test/lisp/erc/erc-tests.el        |  61 ----------
 7 files changed, 279 insertions(+), 90 deletions(-)
 create mode 100644 test/lisp/erc/erc-button-tests.el

diff --git a/etc/ERC-NEWS b/etc/ERC-NEWS
index d2333cbdb89..9794ca1f6ee 100644
--- a/etc/ERC-NEWS
+++ b/etc/ERC-NEWS
@@ -186,10 +186,13 @@ example, requiring the use of 'insert-before-markers' instead of
 changes are encouraged to voice their concerns on the bug list.
 
 *** Miscellaneous changes
-For autoloading purposes, 'Info-goto-node' has been supplanted by
-plain old 'info' in 'erc-button-alist', and two helper macros from GNU
-ELPA's Compat library are now available to third-party modules as
-'erc-compat-call' and 'erc-compat-function'.
+Two helper macros from GNU ELPA's Compat library are now available to
+third-party modules as 'erc-compat-call' and 'erc-compat-function'.
+In the area of buttons, 'Info-goto-node' has been supplanted by plain
+old 'info' in 'erc-button-alist', primarily for autoloading purposes.
+And the "TAB" key is now bound to a new command, 'erc-tab', that only
+calls 'completion-at-point' when point is in the input area and
+module-specific commands, like 'erc-button-next', otherwise.
 
 
 * Changes in ERC 5.5
diff --git a/lisp/erc/erc-button.el b/lisp/erc/erc-button.el
index 33e69f3b0b8..e2447deecde 100644
--- a/lisp/erc/erc-button.el
+++ b/lisp/erc/erc-button.el
@@ -55,11 +55,11 @@ button
   ((erc-button--check-nicknames-entry)
    (add-hook 'erc-insert-modify-hook #'erc-button-add-buttons 'append)
    (add-hook 'erc-send-modify-hook #'erc-button-add-buttons 'append)
-   (add-hook 'erc-complete-functions #'erc-button-next-function)
+   (add-hook 'erc--tab-functions #'erc-button-next)
    (erc--modify-local-map t "<backtab>" #'erc-button-previous))
   ((remove-hook 'erc-insert-modify-hook #'erc-button-add-buttons)
    (remove-hook 'erc-send-modify-hook #'erc-button-add-buttons)
-   (remove-hook 'erc-complete-functions #'erc-button-next-function)
+   (remove-hook 'erc--tab-functions #'erc-button-next)
    (erc--modify-local-map nil "<backtab>" #'erc-button-previous)))
 
 ;;; Variables
@@ -529,6 +529,7 @@ erc-button-press-button
 (defun erc-button-next-function ()
   "Pseudo completion function that actually jumps to the next button.
 For use on `completion-at-point-functions'."
+  (declare (obsolete erc-nickserv-identify "30.1"))
   ;; FIXME: This is an abuse of completion-at-point-functions.
   (when (< (point) (erc-beg-of-input-line))
     (let ((start (point)))
@@ -546,27 +547,73 @@ erc-button-next-function
             (error "No next button"))
           t)))))
 
-(defun erc-button-next ()
-  "Go to the next button in this buffer."
-  (interactive)
-  (let ((f (erc-button-next-function)))
-    (if f (funcall f))))
-
-(defun erc-button-previous ()
-  "Go to the previous button in this buffer."
-  (interactive)
-  (let ((here (point)))
-    (when (< here (erc-beg-of-input-line))
-      (while (and (get-text-property here 'erc-callback)
-                  (not (= here (point-min))))
-        (setq here (1- here)))
-      (while (and (not (get-text-property here 'erc-callback))
-                  (not (= here (point-min))))
-        (setq here (1- here)))
-      (if (> here (point-min))
-          (goto-char here)
-        (error "No previous button"))
-      t)))
+(defvar erc-button--prev-next-predicate-functions
+  '(erc-button--end-of-button-p)
+  "Abnormal hook whose members can return non-nil to continue searching.
+Otherwise, if all members return nil, point will stay at the
+current button.  Called with a single arg, a buffer position
+greater than `point-min' with a text property of `erc-callback'.")
+
+(defun erc-button--end-of-button-p (point)
+  (get-text-property (1- point) 'erc-callback))
+
+(defun erc--button-next (arg)
+  (let* ((nextp (prog1 (>= arg 1) (setq arg (max 1 (abs arg)))))
+         (search-fn (if nextp
+                        #'next-single-char-property-change
+                      #'previous-single-char-property-change))
+         (start (point))
+         (p start))
+    (while (progn
+             ;; Break out of current search context.
+             (when-let ((low (max (point-min) (1- (pos-bol))))
+                        (high (min (point-max) (1+ (pos-eol))))
+                        (prop (get-text-property p 'erc-callback))
+                        (q (if nextp
+                               (text-property-not-all p high
+                                                      'erc-callback prop)
+                             (funcall search-fn p 'erc-callback nil low)))
+                        ((< low q high)))
+               (setq p q))
+             ;; Assume that buttons occur frequently enough that
+             ;; omitting LIMIT is acceptable.
+             (while
+                 (and (setq p (funcall search-fn p 'erc-callback))
+                      (if nextp (< p erc-insert-marker) (/= p (point-min)))
+                      (run-hook-with-args-until-success
+                       'erc-button--prev-next-predicate-functions p)))
+             (and arg
+                  (< (point-min) p erc-insert-marker)
+                  (goto-char p)
+                  (not (zerop (cl-decf arg))))))
+    (when (= (point) start)
+      (user-error (if nextp "No next button" "No previous button")))
+    t))
+
+(defun erc-button-next (&optional arg)
+  "Go to the ARGth next button."
+  (declare (advertised-calling-convention (arg) "30.1"))
+  (interactive "p")
+  (setq arg (pcase arg ((pred listp) (prefix-numeric-value arg)) (_ arg)))
+  (erc--button-next arg))
+
+(defun erc-button-previous (&optional arg)
+  "Go to ARGth previous button."
+  (declare (advertised-calling-convention (arg) "30.1"))
+  (interactive "p")
+  (setq arg (pcase arg ((pred listp) (prefix-numeric-value arg)) (_ arg)))
+  (erc--button-next (- arg)))
+
+(defun erc-button-previous-of-nick (arg)
+  "Go to ARGth previous button for nick at point."
+  (interactive "p")
+  (if-let* ((prop (get-text-property (point) 'erc-data))
+            (erc-button--prev-next-predicate-functions
+             (cons (lambda (p)
+                     (not (equal (get-text-property p 'erc-data) prop)))
+                   erc-button--prev-next-predicate-functions)))
+      (erc--button-next (- arg))
+    (user-error "No nick at point")))
 
 (defun erc-browse-emacswiki (thing)
   "Browse to THING in the emacs-wiki."
diff --git a/lisp/erc/erc-fill.el b/lisp/erc/erc-fill.el
index c29d292abce..767f2bf2c0f 100644
--- a/lisp/erc/erc-fill.el
+++ b/lisp/erc/erc-fill.el
@@ -295,7 +295,9 @@ fill-wrap
            (setq msg (concat msg (and msg " ")
                              (erc-fill--make-module-dependency-msg "button"))))
          (erc-with-server-buffer
-           (erc-button-mode +1))))
+           (erc-button-mode +1)))
+       (add-hook 'erc-button--prev-next-predicate-functions
+                 #'erc-fill--wrap-merged-button-p nil t))
      ;; Set local value of user option (can we avoid this somehow?)
      (unless (eq erc-fill-function #'erc-fill-wrap)
        (setq-local erc-fill-function #'erc-fill-wrap))
@@ -323,6 +325,8 @@ fill-wrap
    (kill-local-variable 'erc-fill--wrap-value)
    (kill-local-variable 'erc-fill-function)
    (kill-local-variable 'erc-fill--wrap-visual-keys)
+   (remove-hook 'erc-button--prev-next-predicate-functions
+                #'erc-fill--wrap-merged-button-p t)
    (remove-function (local 'erc-stamp--insert-date-function)
                     #'erc-fill--wrap-stamp-insert-prefixed-date)
    (visual-line-mode -1))
@@ -405,6 +409,10 @@ erc-fill-wrap
                                `((space :width (- erc-fill--wrap-value ,len))
                                  (space :width erc-fill--wrap-value))))))
 
+;; FIXME use own text property to avoid false positives.
+(defun erc-fill--wrap-merged-button-p (point)
+  (equal "" (get-text-property point 'display)))
+
 ;; This is an experimental helper for third-party modules.  You could,
 ;; for example, use this to automatically resize the prefix to a
 ;; fraction of the window's width on some event change.  Another use
diff --git a/lisp/erc/erc.el b/lisp/erc/erc.el
index a3a9b03a93c..b63dc1009f9 100644
--- a/lisp/erc/erc.el
+++ b/lisp/erc/erc.el
@@ -1229,7 +1229,7 @@ erc-mode-map
     (define-key map "\C-c\C-u" #'erc-kill-input)
     (define-key map "\C-c\C-x" #'erc-quit-server)
     (define-key map "\M-\t" #'ispell-complete-word)
-    (define-key map "\t" #'completion-at-point)
+    (define-key map "\t" #'erc-tab)
 
     ;; Suppress `font-lock-fontify-block' key binding since it
     ;; destroys face properties.
@@ -4665,6 +4665,19 @@ erc-kill-input
         (setq erc-input-ring-index nil))
     (kill-line)))
 
+(defvar erc--tab-functions nil
+  "Functions to try when user hits TAB outside of input area.
+Called with a numeric prefix arg.")
+
+(defun erc-tab (&optional arg)
+  "Call `completion-at-point' when typing in the input area.
+Otherwise call members of `erc--tab-functions' with raw prefix
+ARG until one of them returns non-nil."
+  (interactive "P")
+  (if (>= (point) erc-input-marker)
+      (completion-at-point)
+    (run-hook-with-args-until-success 'erc--tab-functions arg)))
+
 (defun erc-complete-word-at-point ()
   (run-hook-with-args-until-success 'erc-complete-functions))
 
diff --git a/test/lisp/erc/erc-button-tests.el b/test/lisp/erc/erc-button-tests.el
new file mode 100644
index 00000000000..ced08d117bc
--- /dev/null
+++ b/test/lisp/erc/erc-button-tests.el
@@ -0,0 +1,177 @@
+;;; erc-button-tests.el --- Tests for erc-button  -*- lexical-binding:t -*-
+
+;; Copyright (C) 2023 Free Software Foundation, Inc.
+
+;; This file is part of GNU Emacs.
+;;
+;; GNU Emacs is free software: you can redistribute it and/or modify
+;; it under the terms of the GNU General Public License as published
+;; by the Free Software Foundation, either version 3 of the License,
+;; or (at your option) any later version.
+;;
+;; GNU Emacs is distributed in the hope that it will be useful, but
+;; WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+;; General Public License for more details.
+;;
+;; You should have received a copy of the GNU General Public License
+;; along with GNU Emacs.  If not, see <https://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;;; Code:
+
+(require 'erc-button)
+
+(defun erc-button-tests--insert-privmsg (speaker &rest msg-parts)
+  (declare (indent 1))
+  (let ((msg (erc-format-privmessage speaker
+                                     (apply #'concat msg-parts) nil t)))
+    (erc-display-message nil nil (current-buffer) msg)))
+
+(defun erc-button-tests--populate (test)
+  (let ((inhibit-message noninteractive)
+        erc-kill-channel-hook erc-kill-server-hook erc-kill-buffer-hook)
+
+    (with-current-buffer
+        (cl-letf
+            (((symbol-function 'erc-server-connect)
+              (lambda (&rest _)
+                (setq erc-server-process
+                      (start-process "sleep" (current-buffer) "sleep" "1"))
+                (set-process-query-on-exit-flag erc-server-process nil))))
+
+          (erc-open "localhost" 6667 "tester" "Tester" 'connect
+                    nil nil nil nil nil "tester" 'foonet))
+
+      (with-current-buffer (erc--open-target "#chan")
+        (erc-update-channel-member
+         "#chan" "alice" "alice" t nil nil nil nil nil "fake" "~u" nil nil t)
+
+        (erc-update-channel-member
+         "#chan" "bob" "bob" t nil nil nil nil nil "fake" "~u" nil nil t)
+
+        (erc-display-message
+         nil 'notice (current-buffer)
+         (concat "This server is in debug mode and is logging all user I/O. "
+                 "Blah alice (1) bob (2) blah."))
+
+        (funcall test))
+
+      (when noninteractive
+        (kill-buffer "#chan")
+        (kill-buffer)))))
+
+(ert-deftest erc-button-next ()
+  (erc-button-tests--populate
+   (lambda ()
+     (erc-button-tests--insert-privmsg "alice"
+       "(3) bob (4) come, you are a tedious fool: to the purpose.")
+
+     (erc-button-tests--insert-privmsg "bob"
+       "(5) alice (6) Come me to what was done to her.")
+
+     (should (= erc-input-marker (point)))
+
+     ;; Break out of input area
+     (erc-button-previous 1)
+     (should (looking-at (rx "alice (6)")))
+
+     ;; No next button
+     (should-error (erc-button-next 1) :type 'user-error)
+     (should (looking-at (rx "alice (6)")))
+
+     ;; Next with negative arg is equivalent to previous
+     (erc-button-next -1)
+     (should (looking-at (rx "bob> (5)")))
+
+     ;; One past end of button
+     (forward-char 3)
+     (should (looking-at (rx "> (5)")))
+     (should-not (get-text-property (point) 'erc-callback))
+     (erc-button-previous 1)
+     (should (looking-at (rx "bob> (5)")))
+
+     ;; At end of button
+     (forward-char 2)
+     (should (looking-at (rx "b> (5)")))
+     (erc-button-previous 1)
+     (should (looking-at (rx "bob (4)")))
+
+     ;; Skip multiple buttons back
+     (erc-button-previous 2)
+     (should (looking-at (rx "bob (2)")))
+
+     ;; Skip multiple buttons forward
+     (erc-button-next 2)
+     (should (looking-at (rx "bob (4)")))
+
+     ;; No error as long as some progress made
+     (erc-button-previous 100)
+     (should (looking-at (rx "alice (1)")))
+
+     ;; Error when no progress made
+     (should-error (erc-button-previous 1) :type 'user-error)
+     (should (looking-at (rx "alice (1)"))))))
+
+;; See also `erc-scenarios-networks-announced-missing' in
+;; erc-scenarios-misc.el for a more realistic example.
+(ert-deftest erc-button--display-error-notice-with-keys ()
+  (with-current-buffer (get-buffer-create "*fake*")
+    (let ((mode erc-button-mode)
+          (inhibit-message noninteractive)
+          erc-modules
+          erc-kill-channel-hook erc-kill-server-hook erc-kill-buffer-hook)
+      (erc-mode)
+      (setq erc-server-process
+            (start-process "sleep" (current-buffer) "sleep" "1"))
+      (set-process-query-on-exit-flag erc-server-process nil)
+      (erc--initialize-markers (point) nil)
+      (erc-button-mode +1)
+      (should (equal (erc-button--display-error-notice-with-keys
+                      "If \\[erc-bol] fails, "
+                      "see \\[erc-bug] or `erc-mode-map'.")
+                     "*** If C-a fails, see M-x erc-bug or `erc-mode-map'."))
+      (goto-char (point-min))
+
+      (ert-info ("Keymap substitution succeeds")
+        (erc-button-next 1)
+        (should (looking-at "C-a"))
+        (should (eq (get-text-property (point) 'mouse-face) 'highlight))
+        (erc-button-press-button)
+        (with-current-buffer "*Help*"
+          (goto-char (point-min))
+          (should (search-forward "erc-bol" nil t)))
+        (erc-button-next 1)
+        ;; End of interval correct
+        (erc-button-previous 1)
+        (should (looking-at "C-a fails")))
+
+      (ert-info ("Extended command mapping succeeds")
+        (erc-button-next 1)
+        (should (looking-at "M-x erc-bug"))
+        (erc-button-press-button)
+        (should (eq (get-text-property (point) 'mouse-face) 'highlight))
+        (with-current-buffer "*Help*"
+          (goto-char (point-min))
+          (should (search-forward "erc-bug" nil t))))
+
+      (ert-info ("Symbol-description face preserved") ; mutated by d-e-n-w-k
+        (erc-button-next 1)
+        (should (equal (get-text-property (point) 'font-lock-face)
+                       '(erc-button erc-error-face)))
+        (should (eq (get-text-property (point) 'mouse-face) 'highlight))
+        (should (eq erc-button-face 'erc-button))) ; extent evaporates
+
+      (ert-info ("Format when trailing args include non-strings")
+        (should (equal (erc-button--display-error-notice-with-keys
+                        "abc" " %d def" " 45%s" 123 '\6)
+                       "*** abc 123 def 456")))
+
+      (when noninteractive
+        (unless mode
+          (erc-button-mode -1))
+        (kill-buffer "*Help*")
+        (kill-buffer)))))
+
+;;; erc-button-tests.el ends here
diff --git a/test/lisp/erc/erc-fill-tests.el b/test/lisp/erc/erc-fill-tests.el
index f249be8fb86..a72135f14a3 100644
--- a/test/lisp/erc/erc-fill-tests.el
+++ b/test/lisp/erc/erc-fill-tests.el
@@ -94,6 +94,8 @@ erc-fill-tests--wrap-populate
           ;; Defend against non-local exits from `ert-skip'
           (unwind-protect
               (funcall test)
+            (when set-transient-map-timer
+              (timer-event-handler set-transient-map-timer))
             (set-window-buffer (selected-window) original-window-buffer)
             (when noninteractive
               (while-let ((buf (pop erc-fill-tests--buffers)))
diff --git a/test/lisp/erc/erc-tests.el b/test/lisp/erc/erc-tests.el
index 88b9babf206..5aaf7e499e3 100644
--- a/test/lisp/erc/erc-tests.el
+++ b/test/lisp/erc/erc-tests.el
@@ -2110,65 +2110,4 @@ define-erc-module--local
                       (put 'erc-mname-enable 'definition-name 'mname)
                       (put 'erc-mname-disable 'definition-name 'mname))))))
 
-
-;; XXX move erc-button tests to new file if more added.
-(require 'erc-button)
-
-;; See also `erc-scenarios-networks-announced-missing' in
-;; erc-scenarios-misc.el for a more realistic example.
-(ert-deftest erc-button--display-error-notice-with-keys ()
-  (with-current-buffer (get-buffer-create "*fake*")
-    (let ((mode erc-button-mode)
-          (inhibit-message noninteractive)
-          erc-modules
-          erc-kill-channel-hook erc-kill-server-hook erc-kill-buffer-hook)
-      (erc-mode)
-      (erc-tests--set-fake-server-process "sleep" "1")
-      (erc--initialize-markers (point) nil)
-      (erc-button-mode +1)
-      (should (equal (erc-button--display-error-notice-with-keys
-                      "If \\[erc-bol] fails, "
-                      "see \\[erc-bug] or `erc-mode-map'.")
-                     "*** If C-a fails, see M-x erc-bug or `erc-mode-map'."))
-      (goto-char (point-min))
-
-      (ert-info ("Keymap substitution succeeds")
-        (erc-button-next)
-        (should (looking-at "C-a"))
-        (should (eq (get-text-property (point) 'mouse-face) 'highlight))
-        (erc-button-press-button)
-        (with-current-buffer "*Help*"
-          (goto-char (point-min))
-          (should (search-forward "erc-bol" nil t)))
-        (erc-button-next)
-        (erc-button-previous) ; end of interval correct
-        (should (looking-at "a fails")))
-
-      (ert-info ("Extended command mapping succeeds")
-        (erc-button-next)
-        (should (looking-at "M-x erc-bug"))
-        (erc-button-press-button)
-        (should (eq (get-text-property (point) 'mouse-face) 'highlight))
-        (with-current-buffer "*Help*"
-          (goto-char (point-min))
-          (should (search-forward "erc-bug" nil t))))
-
-      (ert-info ("Symbol-description face preserved") ; mutated by d-e-n-w-k
-        (erc-button-next)
-        (should (equal (get-text-property (point) 'font-lock-face)
-                       '(erc-button erc-error-face)))
-        (should (eq (get-text-property (point) 'mouse-face) 'highlight))
-        (should (eq erc-button-face 'erc-button))) ; extent evaporates
-
-      (ert-info ("Format when trailing args include non-strings")
-        (should (equal (erc-button--display-error-notice-with-keys
-                        "abc" " %d def" " 45%s" 123 '\6)
-                       "*** abc 123 def 456")))
-
-      (when noninteractive
-        (unless mode
-          (erc-button-mode -1))
-        (kill-buffer "*Help*")
-        (kill-buffer)))))
-
 ;;; erc-tests.el ends here
-- 
2.39.2