Repository: matplotlib/matplotlib

Issue:
[MNT]: Generalize widget mouse testing
### Summary

Mouse testing for RangeSlider widget is not supported in the current widget testing framework. The current mouse testing for widgets is done using the ```click_and_drag``` function, but it has no support for widgets without 'press' events.

### Proposed fix

The ```click_and_drag``` function should be generalized in order to support more widgets. One solution would be adding support for sending events through the canvas.

This potential solution is mentioned here by @ianhi:
https://github.com/matplotlib/matplotlib/pull/22711#discussion_r836543595




Discussion:
The ```click_and_drag``` function:
https://github.com/matplotlib/matplotlib/blob/2e921df22ba6b2a7782241798b042403c04cbdaf/lib/matplotlib/testing/widgets.py#L92

The ```click_and_drag``` function is currently moving the mouse with these function calls:
https://github.com/matplotlib/matplotlib/blob/2e921df22ba6b2a7782241798b042403c04cbdaf/lib/matplotlib/testing/widgets.py#L112-L114

This is where the current tests will break when using a RangeSlider widget because there is no 'press' event.
https://github.com/matplotlib/matplotlib/blob/2e921df22ba6b2a7782241798b042403c04cbdaf/lib/matplotlib/testing/widgets.py#L88

The click events are defined in ```SliderBase```:
https://github.com/matplotlib/matplotlib/blob/2e921df22ba6b2a7782241798b042403c04cbdaf/lib/matplotlib/widgets.py#L275-L279
I think the core issue is that `do_event` calls methods on the widget rather than actually processing events through the canvas.

So changing:
https://github.com/matplotlib/matplotlib/blob/2e921df22ba6b2a7782241798b042403c04cbdaf/lib/matplotlib/testing/widgets.py#L87-L89

to be something like
```python
def do_event(ax,...):
    event = mock_event(ax, button, xdata, ydata, key, step)
    ax.figure.canvas.callbacks.process(etype, event)
```

would more completely exercise the events mechanism and be more general



Patch:
diff --git a/doc/api/next_api_changes/deprecations/29993-AL.rst b/doc/api/next_api_changes/deprecations/29993-AL.rst
new file mode 100644
index 000000000000..9104fd669325
--- /dev/null
+++ b/doc/api/next_api_changes/deprecations/29993-AL.rst
@@ -0,0 +1,4 @@
+``testing.widgets.mock_event`` and ``testing.widgets.do_event``
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+... are deprecated.  Directly construct Event objects (typically `.MouseEvent`
+or `.KeyEvent`) and pass them to ``canvas.callbacks.process()`` instead.
diff --git a/lib/matplotlib/backend_bases.py b/lib/matplotlib/backend_bases.py
index 626852f2aa34..28c2abdfdf82 100644
--- a/lib/matplotlib/backend_bases.py
+++ b/lib/matplotlib/backend_bases.py
@@ -1411,6 +1411,23 @@ def __init__(self, name, canvas, x, y, button=None, key=None,
         self.step = step
         self.dblclick = dblclick
 
+    @classmethod
+    def _from_ax_coords(cls, name, ax, xy, *args, **kwargs):
+        """
+        Generate a synthetic event at a given axes coordinate.
+
+        This method is intended for creating events during testing.  The event
+        can be emitted by calling its ``_process()`` method.
+
+        args and kwargs are mapped to `.MouseEvent.__init__` parameters,
+        starting with `button`.
+        """
+        x, y = ax.transData.transform(xy)
+        event = cls(name, ax.figure.canvas, x, y, *args, **kwargs)
+        event.inaxes = ax
+        event.xdata, event.ydata = xy  # Force exact xy to avoid fp roundtrip issues.
+        return event
+
     def __str__(self):
         return (f"{self.name}: "
                 f"xy=({self.x}, {self.y}) xydata=({self.xdata}, {self.ydata}) "
@@ -1503,6 +1520,22 @@ def __init__(self, name, canvas, key, x=0, y=0, guiEvent=None):
         super().__init__(name, canvas, x, y, guiEvent=guiEvent)
         self.key = key
 
+    @classmethod
+    def _from_ax_coords(cls, name, ax, xy, key, *args, **kwargs):
+        """
+        Generate a synthetic event at a given axes coordinate.
+
+        This method is intended for creating events during testing.  The event
+        can be emitted by calling its ``_process()`` method.
+        """
+        # Separate from MouseEvent._from_ax_coords instead of being defined in the base
+        # class, due to different parameter order in the constructor signature.
+        x, y = ax.transData.transform(xy)
+        event = cls(name, ax.figure.canvas, key, x, y, *args, **kwargs)
+        event.inaxes = ax
+        event.xdata, event.ydata = xy  # Force exact xy to avoid fp roundtrip issues.
+        return event
+
 
 # Default callback for key events.
 def _key_handler(event):


Test patch:
diff --git a/lib/matplotlib/testing/widgets.py b/lib/matplotlib/testing/widgets.py
index 3962567aa7c0..c528ffb2537c 100644
--- a/lib/matplotlib/testing/widgets.py
+++ b/lib/matplotlib/testing/widgets.py
@@ -8,6 +8,8 @@
 
 from unittest import mock
 
+from matplotlib import _api
+from matplotlib.backend_bases import MouseEvent, KeyEvent
 import matplotlib.pyplot as plt
 
 
@@ -24,6 +26,7 @@ def noop(*args, **kwargs):
     pass
 
 
+@_api.deprecated("3.11", alternative="MouseEvent or KeyEvent")
 def mock_event(ax, button=1, xdata=0, ydata=0, key=None, step=1):
     r"""
     Create a mock event that can stand in for `.Event` and its subclasses.
@@ -65,6 +68,7 @@ def mock_event(ax, button=1, xdata=0, ydata=0, key=None, step=1):
     return event
 
 
+@_api.deprecated("3.11", alternative="callbacks.process(event)")
 def do_event(tool, etype, button=1, xdata=0, ydata=0, key=None, step=1):
     """
     Trigger an event on the given tool.
@@ -105,15 +109,12 @@ def click_and_drag(tool, start, end, key=None):
          An optional key that is pressed during the whole operation
          (see also `.KeyEvent`).
     """
-    if key is not None:
-        # Press key
-        do_event(tool, 'on_key_press', xdata=start[0], ydata=start[1],
-                 button=1, key=key)
+    ax = tool.ax
+    if key is not None:  # Press key
+        KeyEvent._from_ax_coords("key_press_event", ax, start, key)._process()
     # Click, move, and release mouse
-    do_event(tool, 'press', xdata=start[0], ydata=start[1], button=1)
-    do_event(tool, 'onmove', xdata=end[0], ydata=end[1], button=1)
-    do_event(tool, 'release', xdata=end[0], ydata=end[1], button=1)
-    if key is not None:
-        # Release key
-        do_event(tool, 'on_key_release', xdata=end[0], ydata=end[1],
-                 button=1, key=key)
+    MouseEvent._from_ax_coords("button_press_event", ax, start, 1)._process()
+    MouseEvent._from_ax_coords("motion_notify_event", ax, end, 1)._process()
+    MouseEvent._from_ax_coords("button_release_event", ax, end, 1)._process()
+    if key is not None:  # Release key
+        KeyEvent._from_ax_coords("key_release_event", ax, end, key)._process()
diff --git a/lib/matplotlib/tests/test_widgets.py b/lib/matplotlib/tests/test_widgets.py
index 3da678076489..a1b540fb4a28 100644
--- a/lib/matplotlib/tests/test_widgets.py
+++ b/lib/matplotlib/tests/test_widgets.py
@@ -3,13 +3,12 @@
 import operator
 from unittest import mock
 
-from matplotlib.backend_bases import MouseEvent, DrawEvent
+from matplotlib.backend_bases import DrawEvent, KeyEvent, MouseEvent
 import matplotlib.colors as mcolors
 import matplotlib.widgets as widgets
 import matplotlib.pyplot as plt
 from matplotlib.testing.decorators import check_figures_equal, image_comparison
-from matplotlib.testing.widgets import (click_and_drag, do_event, get_ax,
-                                        mock_event, noop)
+from matplotlib.testing.widgets import click_and_drag, get_ax, noop
 
 import numpy as np
 from numpy.testing import assert_allclose
@@ -71,11 +70,10 @@ def test_rectangle_selector(ax, kwargs):
     onselect = mock.Mock(spec=noop, return_value=None)
 
     tool = widgets.RectangleSelector(ax, onselect=onselect, **kwargs)
-    do_event(tool, 'press', xdata=100, ydata=100, button=1)
-    do_event(tool, 'onmove', xdata=199, ydata=199, button=1)
-
+    MouseEvent._from_ax_coords("button_press_event", ax, (100, 100), 1)._process()
+    MouseEvent._from_ax_coords("motion_notify_event", ax, (199, 199), 1)._process()
     # purposely drag outside of axis for release
-    do_event(tool, 'release', xdata=250, ydata=250, button=1)
+    MouseEvent._from_ax_coords("button_release_event", ax, (250, 250), 1)._process()
 
     if kwargs.get('drawtype', None) not in ['line', 'none']:
         assert_allclose(tool.geometry,
@@ -137,7 +135,7 @@ def test_rectangle_drag(ax, drag_from_anywhere, new_center):
     tool = widgets.RectangleSelector(ax, interactive=True,
                                      drag_from_anywhere=drag_from_anywhere)
     # Create rectangle
-    click_and_drag(tool, start=(0, 10), end=(100, 120))
+    click_and_drag(tool, start=(10, 10), end=(90, 120))
     assert tool.center == (50, 65)
     # Drag inside rectangle, but away from centre handle
     #
@@ -178,8 +176,8 @@ def test_rectangle_selector_set_props_handle_props(ax):
 def test_rectangle_resize(ax):
     tool = widgets.RectangleSelector(ax, interactive=True)
     # Create rectangle
-    click_and_drag(tool, start=(0, 10), end=(100, 120))
-    assert tool.extents == (0.0, 100.0, 10.0, 120.0)
+    click_and_drag(tool, start=(10, 10), end=(100, 120))
+    assert tool.extents == (10.0, 100.0, 10.0, 120.0)
 
     # resize NE handle
     extents = tool.extents
@@ -446,11 +444,11 @@ def test_rectangle_rotate(ax, selector_class):
     assert len(tool._state) == 0
 
     # Rotate anticlockwise using top-right corner
-    do_event(tool, 'on_key_press', key='r')
+    KeyEvent("key_press_event", ax.figure.canvas, "r")._process()
     assert tool._state == {'rotate'}
     assert len(tool._state) == 1
     click_and_drag(tool, start=(130, 140), end=(120, 145))
-    do_event(tool, 'on_key_press', key='r')
+    KeyEvent("key_press_event", ax.figure.canvas, "r")._process()
     assert len(tool._state) == 0
     # Extents shouldn't change (as shape of rectangle hasn't changed)
     assert tool.extents == (100, 130, 100, 140)
@@ -636,10 +634,10 @@ def test_span_selector(ax, orientation, onmove_callback, kwargs):
     tax = ax.twinx()
 
     tool = widgets.SpanSelector(ax, onselect, orientation, **kwargs)
-    do_event(tool, 'press', xdata=100, ydata=100, button=1)
+    MouseEvent._from_ax_coords("button_press_event", ax, (100, 100), 1)._process()
     # move outside of axis
-    do_event(tool, 'onmove', xdata=199, ydata=199, button=1)
-    do_event(tool, 'release', xdata=250, ydata=250, button=1)
+    MouseEvent._from_ax_coords("motion_notify_event", ax, (199, 199), 1)._process()
+    MouseEvent._from_ax_coords("button_release_event", ax, (250, 250), 1)._process()
 
     onselect.assert_called_once_with(100, 199)
     if onmove_callback:
@@ -783,7 +781,7 @@ def test_selector_clear(ax, selector):
     click_and_drag(tool, start=(130, 130), end=(130, 130))
     assert tool._selection_completed
 
-    do_event(tool, 'on_key_press', key='escape')
+    KeyEvent("key_press_event", ax.figure.canvas, "escape")._process()
     assert not tool._selection_completed
 
 
@@ -905,10 +903,8 @@ def mean(vmin, vmax):
 
     # Add span selector and check that the line is draw after it was updated
     # by the callback
-    press_data = [1, 2]
-    move_data = [2, 2]
-    do_event(span, 'press', xdata=press_data[0], ydata=press_data[1], button=1)
-    do_event(span, 'onmove', xdata=move_data[0], ydata=move_data[1], button=1)
+    MouseEvent._from_ax_coords("button_press_event", ax, (1, 2), 1)._process()
+    MouseEvent._from_ax_coords("motion_notify_event", ax, (2, 2), 1)._process()
     assert span._get_animated_artists() == (ln, ln2)
     assert ln.stale is False
     assert ln2.stale
@@ -918,16 +914,12 @@ def mean(vmin, vmax):
 
     # Change span selector and check that the line is drawn/updated after its
     # value was updated by the callback
-    press_data = [4, 0]
-    move_data = [5, 2]
-    release_data = [5, 2]
-    do_event(span, 'press', xdata=press_data[0], ydata=press_data[1], button=1)
-    do_event(span, 'onmove', xdata=move_data[0], ydata=move_data[1], button=1)
+    MouseEvent._from_ax_coords("button_press_event", ax, (4, 0), 1)._process()
+    MouseEvent._from_ax_coords("motion_notify_event", ax, (5, 2), 1)._process()
     assert ln.stale is False
     assert ln2.stale
     assert_allclose(ln2.get_ydata(), -0.9424150707548072)
-    do_event(span, 'release', xdata=release_data[0],
-             ydata=release_data[1], button=1)
+    MouseEvent._from_ax_coords("button_release_event", ax, (5, 2), 1)._process()
     assert ln2.stale is False
 
 
@@ -988,9 +980,9 @@ def test_lasso_selector(ax, kwargs):
     onselect = mock.Mock(spec=noop, return_value=None)
 
     tool = widgets.LassoSelector(ax, onselect=onselect, **kwargs)
-    do_event(tool, 'press', xdata=100, ydata=100, button=1)
-    do_event(tool, 'onmove', xdata=125, ydata=125, button=1)
-    do_event(tool, 'release', xdata=150, ydata=150, button=1)
+    MouseEvent._from_ax_coords("button_press_event", ax, (100, 100), 1)._process()
+    MouseEvent._from_ax_coords("motion_notify_event", ax, (125, 125), 1)._process()
+    MouseEvent._from_ax_coords("button_release_event", ax, (150, 150), 1)._process()
 
     onselect.assert_called_once_with([(100, 100), (125, 125), (150, 150)])
 
@@ -1066,7 +1058,7 @@ def test_TextBox(ax, toolbar):
 
     assert tool.text == ''
 
-    do_event(tool, '_click')
+    MouseEvent._from_ax_coords("button_press_event", ax, (.5, .5), 1)._process()
 
     tool.set_val('x**2')
 
@@ -1078,9 +1070,9 @@ def test_TextBox(ax, toolbar):
 
     assert submit_event.call_count == 2
 
-    do_event(tool, '_click', xdata=.5, ydata=.5)  # Ensure the click is in the axes.
-    do_event(tool, '_keypress', key='+')
-    do_event(tool, '_keypress', key='5')
+    MouseEvent._from_ax_coords("button_press_event", ax, (.5, .5), 1)._process()
+    KeyEvent("key_press_event", ax.figure.canvas, "+")._process()
+    KeyEvent("key_press_event", ax.figure.canvas, "5")._process()
 
     assert text_change_event.call_count == 3
 
@@ -1343,162 +1335,160 @@ def test_range_slider_same_init_values(orientation):
     assert_allclose(box.get_points().flatten()[idx], [0, 0.25, 0, 0.75])
 
 
-def check_polygon_selector(event_sequence, expected_result, selections_count,
-                           **kwargs):
+def check_polygon_selector(events, expected, selections_count, **kwargs):
     """
     Helper function to test Polygon Selector.
 
     Parameters
     ----------
-    event_sequence : list of tuples (etype, dict())
-        A sequence of events to perform. The sequence is a list of tuples
-        where the first element of the tuple is an etype (e.g., 'onmove',
-        'press', etc.), and the second element of the tuple is a dictionary of
-         the arguments for the event (e.g., xdata=5, key='shift', etc.).
-    expected_result : list of vertices (xdata, ydata)
-        The list of vertices that are expected to result from the event
-        sequence.
+    events : list[MouseEvent]
+        A sequence of events to perform.
+    expected : list of vertices (xdata, ydata)
+        The list of vertices expected to result from the event sequence.
     selections_count : int
         Wait for the tool to call its `onselect` function `selections_count`
-        times, before comparing the result to the `expected_result`
+        times, before comparing the result to the `expected`
     **kwargs
         Keyword arguments are passed to PolygonSelector.
     """
-    ax = get_ax()
-
     onselect = mock.Mock(spec=noop, return_value=None)
 
+    ax = events[0].canvas.figure.axes[0]
     tool = widgets.PolygonSelector(ax, onselect=onselect, **kwargs)
 
-    for (etype, event_args) in event_sequence:
-        do_event(tool, etype, **event_args)
+    for event in events:
+        event._process()
 
     assert onselect.call_count == selections_count
-    assert onselect.call_args == ((expected_result, ), {})
+    assert onselect.call_args == ((expected, ), {})
 
 
-def polygon_place_vertex(xdata, ydata):
-    return [('onmove', dict(xdata=xdata, ydata=ydata)),
-            ('press', dict(xdata=xdata, ydata=ydata)),
-            ('release', dict(xdata=xdata, ydata=ydata))]
+def polygon_place_vertex(ax, xy):
+    return [
+        MouseEvent._from_ax_coords("motion_notify_event", ax, xy),
+        MouseEvent._from_ax_coords("button_press_event", ax, xy, 1),
+        MouseEvent._from_ax_coords("button_release_event", ax, xy, 1),
+    ]
 
 
-def polygon_remove_vertex(xdata, ydata):
-    return [('onmove', dict(xdata=xdata, ydata=ydata)),
-            ('press', dict(xdata=xdata, ydata=ydata, button=3)),
-            ('release', dict(xdata=xdata, ydata=ydata, button=3))]
+def polygon_remove_vertex(ax, xy):
+    return [
+        MouseEvent._from_ax_coords("motion_notify_event", ax, xy),
+        MouseEvent._from_ax_coords("button_press_event", ax, xy, 3),
+        MouseEvent._from_ax_coords("button_release_event", ax, xy, 3),
+    ]
 
 
 @pytest.mark.parametrize('draw_bounding_box', [False, True])
-def test_polygon_selector(draw_bounding_box):
+def test_polygon_selector(ax, draw_bounding_box):
     check_selector = functools.partial(
         check_polygon_selector, draw_bounding_box=draw_bounding_box)
 
     # Simple polygon
     expected_result = [(50, 50), (150, 50), (50, 150)]
     event_sequence = [
-        *polygon_place_vertex(50, 50),
-        *polygon_place_vertex(150, 50),
-        *polygon_place_vertex(50, 150),
-        *polygon_place_vertex(50, 50),
+        *polygon_place_vertex(ax, (50, 50)),
+        *polygon_place_vertex(ax, (150, 50)),
+        *polygon_place_vertex(ax, (50, 150)),
+        *polygon_place_vertex(ax, (50, 50)),
     ]
     check_selector(event_sequence, expected_result, 1)
 
     # Move first vertex before completing the polygon.
     expected_result = [(75, 50), (150, 50), (50, 150)]
     event_sequence = [
-        *polygon_place_vertex(50, 50),
-        *polygon_place_vertex(150, 50),
-        ('on_key_press', dict(key='control')),
-        ('onmove', dict(xdata=50, ydata=50)),
-        ('press', dict(xdata=50, ydata=50)),
-        ('onmove', dict(xdata=75, ydata=50)),
-        ('release', dict(xdata=75, ydata=50)),
-        ('on_key_release', dict(key='control')),
-        *polygon_place_vertex(50, 150),
-        *polygon_place_vertex(75, 50),
+        *polygon_place_vertex(ax, (50, 50)),
+        *polygon_place_vertex(ax, (150, 50)),
+        KeyEvent("key_press_event", ax.figure.canvas, "control"),
+        MouseEvent._from_ax_coords("motion_notify_event", ax, (50, 50)),
+        MouseEvent._from_ax_coords("button_press_event", ax, (50, 50), 1),
+        MouseEvent._from_ax_coords("motion_notify_event", ax, (75, 50)),
+        MouseEvent._from_ax_coords("button_release_event", ax, (75, 50), 1),
+        KeyEvent("key_release_event", ax.figure.canvas, "control"),
+        *polygon_place_vertex(ax, (50, 150)),
+        *polygon_place_vertex(ax, (75, 50)),
     ]
     check_selector(event_sequence, expected_result, 1)
 
     # Move first two vertices at once before completing the polygon.
     expected_result = [(50, 75), (150, 75), (50, 150)]
     event_sequence = [
-        *polygon_place_vertex(50, 50),
-        *polygon_place_vertex(150, 50),
-        ('on_key_press', dict(key='shift')),
-        ('onmove', dict(xdata=100, ydata=100)),
-        ('press', dict(xdata=100, ydata=100)),
-        ('onmove', dict(xdata=100, ydata=125)),
-        ('release', dict(xdata=100, ydata=125)),
-        ('on_key_release', dict(key='shift')),
-        *polygon_place_vertex(50, 150),
-        *polygon_place_vertex(50, 75),
+        *polygon_place_vertex(ax, (50, 50)),
+        *polygon_place_vertex(ax, (150, 50)),
+        KeyEvent("key_press_event", ax.figure.canvas, "shift"),
+        MouseEvent._from_ax_coords("motion_notify_event", ax, (100, 100)),
+        MouseEvent._from_ax_coords("button_press_event", ax, (100, 100), 1),
+        MouseEvent._from_ax_coords("motion_notify_event", ax, (100, 125)),
+        MouseEvent._from_ax_coords("button_release_event", ax, (100, 125), 1),
+        KeyEvent("key_release_event", ax.figure.canvas, "shift"),
+        *polygon_place_vertex(ax, (50, 150)),
+        *polygon_place_vertex(ax, (50, 75)),
     ]
     check_selector(event_sequence, expected_result, 1)
 
     # Move first vertex after completing the polygon.
-    expected_result = [(75, 50), (150, 50), (50, 150)]
+    expected_result = [(85, 50), (150, 50), (50, 150)]
     event_sequence = [
-        *polygon_place_vertex(50, 50),
-        *polygon_place_vertex(150, 50),
-        *polygon_place_vertex(50, 150),
-        *polygon_place_vertex(50, 50),
-        ('onmove', dict(xdata=50, ydata=50)),
-        ('press', dict(xdata=50, ydata=50)),
-        ('onmove', dict(xdata=75, ydata=50)),
-        ('release', dict(xdata=75, ydata=50)),
+        *polygon_place_vertex(ax, (60, 50)),
+        *polygon_place_vertex(ax, (150, 50)),
+        *polygon_place_vertex(ax, (50, 150)),
+        *polygon_place_vertex(ax, (60, 50)),
+        MouseEvent._from_ax_coords("motion_notify_event", ax, (60, 50)),
+        MouseEvent._from_ax_coords("button_press_event", ax, (60, 50), 1),
+        MouseEvent._from_ax_coords("motion_notify_event", ax, (85, 50)),
+        MouseEvent._from_ax_coords("button_release_event", ax, (85, 50), 1),
     ]
     check_selector(event_sequence, expected_result, 2)
 
     # Move all vertices after completing the polygon.
     expected_result = [(75, 75), (175, 75), (75, 175)]
     event_sequence = [
-        *polygon_place_vertex(50, 50),
-        *polygon_place_vertex(150, 50),
-        *polygon_place_vertex(50, 150),
-        *polygon_place_vertex(50, 50),
-        ('on_key_press', dict(key='shift')),
-        ('onmove', dict(xdata=100, ydata=100)),
-        ('press', dict(xdata=100, ydata=100)),
-        ('onmove', dict(xdata=125, ydata=125)),
-        ('release', dict(xdata=125, ydata=125)),
-        ('on_key_release', dict(key='shift')),
+        *polygon_place_vertex(ax, (50, 50)),
+        *polygon_place_vertex(ax, (150, 50)),
+        *polygon_place_vertex(ax, (50, 150)),
+        *polygon_place_vertex(ax, (50, 50)),
+        KeyEvent("key_press_event", ax.figure.canvas, "shift"),
+        MouseEvent._from_ax_coords("motion_notify_event", ax, (100, 100)),
+        MouseEvent._from_ax_coords("button_press_event", ax, (100, 100), 1),
+        MouseEvent._from_ax_coords("motion_notify_event", ax, (125, 125)),
+        MouseEvent._from_ax_coords("button_release_event", ax, (125, 125), 1),
+        KeyEvent("key_release_event", ax.figure.canvas, "shift"),
     ]
     check_selector(event_sequence, expected_result, 2)
 
     # Try to move a vertex and move all before placing any vertices.
     expected_result = [(50, 50), (150, 50), (50, 150)]
     event_sequence = [
-        ('on_key_press', dict(key='control')),
-        ('onmove', dict(xdata=100, ydata=100)),
-        ('press', dict(xdata=100, ydata=100)),
-        ('onmove', dict(xdata=125, ydata=125)),
-        ('release', dict(xdata=125, ydata=125)),
-        ('on_key_release', dict(key='control')),
-        ('on_key_press', dict(key='shift')),
-        ('onmove', dict(xdata=100, ydata=100)),
-        ('press', dict(xdata=100, ydata=100)),
-        ('onmove', dict(xdata=125, ydata=125)),
-        ('release', dict(xdata=125, ydata=125)),
-        ('on_key_release', dict(key='shift')),
-        *polygon_place_vertex(50, 50),
-        *polygon_place_vertex(150, 50),
-        *polygon_place_vertex(50, 150),
-        *polygon_place_vertex(50, 50),
+        KeyEvent("key_press_event", ax.figure.canvas, "control"),
+        MouseEvent._from_ax_coords("motion_notify_event", ax, (100, 100)),
+        MouseEvent._from_ax_coords("button_press_event", ax, (100, 100), 1),
+        MouseEvent._from_ax_coords("motion_notify_event", ax, (125, 125)),
+        MouseEvent._from_ax_coords("button_release_event", ax, (125, 125), 1),
+        KeyEvent("key_release_event", ax.figure.canvas, "control"),
+        KeyEvent("key_press_event", ax.figure.canvas, "shift"),
+        MouseEvent._from_ax_coords("motion_notify_event", ax, (100, 100)),
+        MouseEvent._from_ax_coords("button_press_event", ax, (100, 100), 1),
+        MouseEvent._from_ax_coords("motion_notify_event", ax, (125, 125)),
+        MouseEvent._from_ax_coords("button_release_event", ax, (125, 125), 1),
+        KeyEvent("key_release_event", ax.figure.canvas, "shift"),
+        *polygon_place_vertex(ax, (50, 50)),
+        *polygon_place_vertex(ax, (150, 50)),
+        *polygon_place_vertex(ax, (50, 150)),
+        *polygon_place_vertex(ax, (50, 50)),
     ]
     check_selector(event_sequence, expected_result, 1)
 
     # Try to place vertex out-of-bounds, then reset, and start a new polygon.
     expected_result = [(50, 50), (150, 50), (50, 150)]
     event_sequence = [
-        *polygon_place_vertex(50, 50),
-        *polygon_place_vertex(250, 50),
-        ('on_key_press', dict(key='escape')),
-        ('on_key_release', dict(key='escape')),
-        *polygon_place_vertex(50, 50),
-        *polygon_place_vertex(150, 50),
-        *polygon_place_vertex(50, 150),
-        *polygon_place_vertex(50, 50),
+        *polygon_place_vertex(ax, (50, 50)),
+        *polygon_place_vertex(ax, (250, 50)),
+        KeyEvent("key_press_event", ax.figure.canvas, "escape"),
+        KeyEvent("key_release_event", ax.figure.canvas, "escape"),
+        *polygon_place_vertex(ax, (50, 50)),
+        *polygon_place_vertex(ax, (150, 50)),
+        *polygon_place_vertex(ax, (50, 150)),
+        *polygon_place_vertex(ax, (50, 50)),
     ]
     check_selector(event_sequence, expected_result, 1)
 
@@ -1510,15 +1500,13 @@ def test_polygon_selector_set_props_handle_props(ax, draw_bounding_box):
                                    handle_props=dict(alpha=0.5),
                                    draw_bounding_box=draw_bounding_box)
 
-    event_sequence = [
-        *polygon_place_vertex(50, 50),
-        *polygon_place_vertex(150, 50),
-        *polygon_place_vertex(50, 150),
-        *polygon_place_vertex(50, 50),
-    ]
-
-    for (etype, event_args) in event_sequence:
-        do_event(tool, etype, **event_args)
+    for event in [
+        *polygon_place_vertex(ax, (50, 50)),
+        *polygon_place_vertex(ax, (150, 50)),
+        *polygon_place_vertex(ax, (50, 150)),
+        *polygon_place_vertex(ax, (50, 50)),
+    ]:
+        event._process()
 
     artist = tool._selection_artist
     assert artist.get_color() == 'b'
@@ -1549,17 +1537,17 @@ def test_rect_visibility(fig_test, fig_ref):
 # Change the order that the extra point is inserted in
 @pytest.mark.parametrize('idx', [1, 2, 3])
 @pytest.mark.parametrize('draw_bounding_box', [False, True])
-def test_polygon_selector_remove(idx, draw_bounding_box):
+def test_polygon_selector_remove(ax, idx, draw_bounding_box):
     verts = [(50, 50), (150, 50), (50, 150)]
-    event_sequence = [polygon_place_vertex(*verts[0]),
-                      polygon_place_vertex(*verts[1]),
-                      polygon_place_vertex(*verts[2]),
+    event_sequence = [polygon_place_vertex(ax, verts[0]),
+                      polygon_place_vertex(ax, verts[1]),
+                      polygon_place_vertex(ax, verts[2]),
                       # Finish the polygon
-                      polygon_place_vertex(*verts[0])]
+                      polygon_place_vertex(ax, verts[0])]
     # Add an extra point
-    event_sequence.insert(idx, polygon_place_vertex(200, 200))
+    event_sequence.insert(idx, polygon_place_vertex(ax, (200, 200)))
     # Remove the extra point
-    event_sequence.append(polygon_remove_vertex(200, 200))
+    event_sequence.append(polygon_remove_vertex(ax, (200, 200)))
     # Flatten list of lists
     event_sequence = functools.reduce(operator.iadd, event_sequence, [])
     check_polygon_selector(event_sequence, verts, 2,
@@ -1567,14 +1555,14 @@ def test_polygon_selector_remove(idx, draw_bounding_box):
 
 
 @pytest.mark.parametrize('draw_bounding_box', [False, True])
-def test_polygon_selector_remove_first_point(draw_bounding_box):
+def test_polygon_selector_remove_first_point(ax, draw_bounding_box):
     verts = [(50, 50), (150, 50), (50, 150)]
     event_sequence = [
-        *polygon_place_vertex(*verts[0]),
-        *polygon_place_vertex(*verts[1]),
-        *polygon_place_vertex(*verts[2]),
-        *polygon_place_vertex(*verts[0]),
-        *polygon_remove_vertex(*verts[0]),
+        *polygon_place_vertex(ax, verts[0]),
+        *polygon_place_vertex(ax, verts[1]),
+        *polygon_place_vertex(ax, verts[2]),
+        *polygon_place_vertex(ax, verts[0]),
+        *polygon_remove_vertex(ax, verts[0]),
     ]
     check_polygon_selector(event_sequence, verts[1:], 2,
                            draw_bounding_box=draw_bounding_box)
@@ -1584,20 +1572,20 @@ def test_polygon_selector_remove_first_point(draw_bounding_box):
 def test_polygon_selector_redraw(ax, draw_bounding_box):
     verts = [(50, 50), (150, 50), (50, 150)]
     event_sequence = [
-        *polygon_place_vertex(*verts[0]),
-        *polygon_place_vertex(*verts[1]),
-        *polygon_place_vertex(*verts[2]),
-        *polygon_place_vertex(*verts[0]),
+        *polygon_place_vertex(ax, verts[0]),
+        *polygon_place_vertex(ax, verts[1]),
+        *polygon_place_vertex(ax, verts[2]),
+        *polygon_place_vertex(ax, verts[0]),
         # Polygon completed, now remove first two verts.
-        *polygon_remove_vertex(*verts[1]),
-        *polygon_remove_vertex(*verts[2]),
+        *polygon_remove_vertex(ax, verts[1]),
+        *polygon_remove_vertex(ax, verts[2]),
         # At this point the tool should be reset so we can add more vertices.
-        *polygon_place_vertex(*verts[1]),
+        *polygon_place_vertex(ax, verts[1]),
     ]
 
     tool = widgets.PolygonSelector(ax, draw_bounding_box=draw_bounding_box)
-    for (etype, event_args) in event_sequence:
-        do_event(tool, etype, **event_args)
+    for event in event_sequence:
+        event._process()
     # After removing two verts, only one remains, and the
     # selector should be automatically reset
     assert tool.verts == verts[0:2]
@@ -1615,14 +1603,13 @@ def test_polygon_selector_verts_setter(fig_test, fig_ref, draw_bounding_box):
 
     ax_ref = fig_ref.add_subplot()
     tool_ref = widgets.PolygonSelector(ax_ref, draw_bounding_box=draw_bounding_box)
-    event_sequence = [
-        *polygon_place_vertex(*verts[0]),
-        *polygon_place_vertex(*verts[1]),
-        *polygon_place_vertex(*verts[2]),
-        *polygon_place_vertex(*verts[0]),
-    ]
-    for (etype, event_args) in event_sequence:
-        do_event(tool_ref, etype, **event_args)
+    for event in [
+        *polygon_place_vertex(ax_ref, verts[0]),
+        *polygon_place_vertex(ax_ref, verts[1]),
+        *polygon_place_vertex(ax_ref, verts[2]),
+        *polygon_place_vertex(ax_ref, verts[0]),
+    ]:
+        event._process()
 
 
 def test_polygon_selector_box(ax):
@@ -1630,40 +1617,29 @@ def test_polygon_selector_box(ax):
     ax.set(xlim=(-10, 50), ylim=(-10, 50))
     verts = [(20, 0), (0, 20), (20, 40), (40, 20)]
     event_sequence = [
-        *polygon_place_vertex(*verts[0]),
-        *polygon_place_vertex(*verts[1]),
-        *polygon_place_vertex(*verts[2]),
-        *polygon_place_vertex(*verts[3]),
-        *polygon_place_vertex(*verts[0]),
+        *polygon_place_vertex(ax, verts[0]),
+        *polygon_place_vertex(ax, verts[1]),
+        *polygon_place_vertex(ax, verts[2]),
+        *polygon_place_vertex(ax, verts[3]),
+        *polygon_place_vertex(ax, verts[0]),
     ]
 
     # Create selector
     tool = widgets.PolygonSelector(ax, draw_bounding_box=True)
-    for (etype, event_args) in event_sequence:
-        do_event(tool, etype, **event_args)
-
-    # In order to trigger the correct callbacks, trigger events on the canvas
-    # instead of the individual tools
-    t = ax.transData
-    canvas = ax.get_figure(root=True).canvas
+    for event in event_sequence:
+        event._process()
 
     # Scale to half size using the top right corner of the bounding box
-    MouseEvent(
-        "button_press_event", canvas, *t.transform((40, 40)), 1)._process()
-    MouseEvent(
-        "motion_notify_event", canvas, *t.transform((20, 20)))._process()
-    MouseEvent(
-        "button_release_event", canvas, *t.transform((20, 20)), 1)._process()
+    MouseEvent._from_ax_coords("button_press_event", ax, (40, 40), 1)._process()
+    MouseEvent._from_ax_coords("motion_notify_event", ax, (20, 20))._process()
+    MouseEvent._from_ax_coords("button_release_event", ax, (20, 20), 1)._process()
     np.testing.assert_allclose(
         tool.verts, [(10, 0), (0, 10), (10, 20), (20, 10)])
 
     # Move using the center of the bounding box
-    MouseEvent(
-        "button_press_event", canvas, *t.transform((10, 10)), 1)._process()
-    MouseEvent(
-        "motion_notify_event", canvas, *t.transform((30, 30)))._process()
-    MouseEvent(
-        "button_release_event", canvas, *t.transform((30, 30)), 1)._process()
+    MouseEvent._from_ax_coords("button_press_event", ax, (10, 10), 1)._process()
+    MouseEvent._from_ax_coords("motion_notify_event", ax, (30, 30))._process()
+    MouseEvent._from_ax_coords("button_release_event", ax, (30, 30), 1)._process()
     np.testing.assert_allclose(
         tool.verts, [(30, 20), (20, 30), (30, 40), (40, 30)])
 
@@ -1671,10 +1647,8 @@ def test_polygon_selector_box(ax):
     np.testing.assert_allclose(
         tool._box.extents, (20.0, 40.0, 20.0, 40.0))
 
-    MouseEvent(
-        "button_press_event", canvas, *t.transform((30, 20)), 3)._process()
-    MouseEvent(
-        "button_release_event", canvas, *t.transform((30, 20)), 3)._process()
+    MouseEvent._from_ax_coords("button_press_event", ax, (30, 20), 3)._process()
+    MouseEvent._from_ax_coords("button_release_event", ax, (30, 20), 3)._process()
     np.testing.assert_allclose(
         tool.verts, [(20, 30), (30, 40), (40, 30)])
     np.testing.assert_allclose(
@@ -1687,9 +1661,9 @@ def test_polygon_selector_clear_method(ax):
 
     for result in ([(50, 50), (150, 50), (50, 150), (50, 50)],
                    [(50, 50), (100, 50), (50, 150), (50, 50)]):
-        for x, y in result:
-            for etype, event_args in polygon_place_vertex(x, y):
-                do_event(tool, etype, **event_args)
+        for xy in result:
+            for event in polygon_place_vertex(ax, xy):
+                event._process()
 
         artist = tool._selection_artist
 
@@ -1720,11 +1694,7 @@ def test_MultiCursor(horizOn, vertOn):
     assert len(multi.vlines) == 2
     assert len(multi.hlines) == 2
 
-    # mock a motion_notify_event
-    # Can't use `do_event` as that helper requires the widget
-    # to have a single .ax attribute.
-    event = mock_event(ax1, xdata=.5, ydata=.25)
-    multi.onmove(event)
+    MouseEvent._from_ax_coords("motion_notify_event", ax1, (.5, .25))._process()
     # force a draw + draw event to exercise clear
     fig.canvas.draw()
 
@@ -1742,8 +1712,7 @@ def test_MultiCursor(horizOn, vertOn):
     # After toggling settings, the opposite lines should be visible after move.
     multi.horizOn = not multi.horizOn
     multi.vertOn = not multi.vertOn
-    event = mock_event(ax1, xdata=.5, ydata=.25)
-    multi.onmove(event)
+    MouseEvent._from_ax_coords("motion_notify_event", ax1, (.5, .25))._process()
     assert len([line for line in multi.vlines if line.get_visible()]) == (
         0 if vertOn else 2)
     assert len([line for line in multi.hlines if line.get_visible()]) == (
@@ -1751,8 +1720,7 @@ def test_MultiCursor(horizOn, vertOn):
 
     # test a move event in an Axes not part of the MultiCursor
     # the lines in ax1 and ax2 should not have moved.
-    event = mock_event(ax3, xdata=.75, ydata=.75)
-    multi.onmove(event)
+    MouseEvent._from_ax_coords("motion_notify_event", ax3, (.75, .75))._process()
     for l in multi.vlines:
         assert l.get_xdata() == (.5, .5)
     for l in multi.hlines:
diff --git a/lib/mpl_toolkits/mplot3d/tests/test_axes3d.py b/lib/mpl_toolkits/mplot3d/tests/test_axes3d.py
index cd45c8e33a6f..45ec1da9f715 100644
--- a/lib/mpl_toolkits/mplot3d/tests/test_axes3d.py
+++ b/lib/mpl_toolkits/mplot3d/tests/test_axes3d.py
@@ -13,7 +13,6 @@
 from matplotlib import cm
 from matplotlib import colors as mcolors, patches as mpatch
 from matplotlib.testing.decorators import image_comparison, check_figures_equal
-from matplotlib.testing.widgets import mock_event
 from matplotlib.collections import LineCollection, PolyCollection
 from matplotlib.patches import Circle, PathPatch
 from matplotlib.path import Path
@@ -2012,11 +2011,11 @@ def test_rotate(style):
             ax.figure.canvas.draw()
 
             # drag mouse to change orientation
-            ax._button_press(
-                mock_event(ax, button=MouseButton.LEFT, xdata=0, ydata=0))
-            ax._on_move(
-                mock_event(ax, button=MouseButton.LEFT,
-                           xdata=s*dx*ax._pseudo_w, ydata=s*dy*ax._pseudo_h))
+            MouseEvent._from_ax_coords(
+                "button_press_event", ax, (0, 0), MouseButton.LEFT)._process()
+            MouseEvent._from_ax_coords(
+                "motion_notify_event", ax, (s*dx*ax._pseudo_w, s*dy*ax._pseudo_h),
+                MouseButton.LEFT)._process()
             ax.figure.canvas.draw()
 
             c = np.sqrt(3)/2
@@ -2076,10 +2075,10 @@ def convert_lim(dmin, dmax):
     z_center0, z_range0 = convert_lim(*ax.get_zlim3d())
 
     # move mouse diagonally to pan along all axis.
-    ax._button_press(
-        mock_event(ax, button=MouseButton.MIDDLE, xdata=0, ydata=0))
-    ax._on_move(
-        mock_event(ax, button=MouseButton.MIDDLE, xdata=1, ydata=1))
+    MouseEvent._from_ax_coords(
+        "button_press_event", ax, (0, 0), MouseButton.MIDDLE)._process()
+    MouseEvent._from_ax_coords(
+        "motion_notify_event", ax, (1, 1), MouseButton.MIDDLE)._process()
 
     x_center, x_range = convert_lim(*ax.get_xlim3d())
     y_center, y_range = convert_lim(*ax.get_ylim3d())
@@ -2553,11 +2552,10 @@ def test_on_move_vertical_axis(vertical_axis: str) -> None:
     ax.get_figure().canvas.draw()
 
     proj_before = ax.get_proj()
-    event_click = mock_event(ax, button=MouseButton.LEFT, xdata=0, ydata=1)
-    ax._button_press(event_click)
-
-    event_move = mock_event(ax, button=MouseButton.LEFT, xdata=0.5, ydata=0.8)
-    ax._on_move(event_move)
+    MouseEvent._from_ax_coords(
+        "button_press_event", ax, (0, 1), MouseButton.LEFT)._process()
+    MouseEvent._from_ax_coords(
+        "motion_notify_event", ax, (.5, .8), MouseButton.LEFT)._process()
 
     assert ax._axis_names.index(vertical_axis) == ax._vertical_axis